feat(api): add REST endpoints for go-agentic Client
16 endpoints matching the go-agentic Client contract: - Plans: list, get, create, update status, archive - Phases: get, update status, checkpoint, task update, task toggle - Sessions: list, get, start, end, continue - Health: /v1/health ping Routes at /v1/* with AgentApiAuth Bearer token middleware. Permission-scoped: plans.read, plans.write, phases.write, sessions.write. Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
2ce8a02ce6
commit
f15093843b
3 changed files with 671 additions and 69 deletions
22
Boot.php
22
Boot.php
|
|
@ -123,6 +123,25 @@ class Boot extends ServiceProvider
|
|||
// Event-driven handlers (for lazy loading once event system is integrated)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Handle API routes registration event.
|
||||
*
|
||||
* Registers REST API endpoints for go-agentic Client consumption.
|
||||
* Routes are prefixed with /api by the API middleware group.
|
||||
*/
|
||||
public function onApiRoutes(ApiRoutesRegistering $event): void
|
||||
{
|
||||
// Register agent API auth middleware alias
|
||||
$event->middleware('agent.auth', Middleware\AgentApiAuth::class);
|
||||
|
||||
// Agent API routes are registered at /v1/* (no /api prefix)
|
||||
// because the go-agentic Client uses BaseURL + "/v1/plans" directly.
|
||||
// We register here but with our own Route group to avoid the
|
||||
// automatic /api prefix from LifecycleEventProvider.
|
||||
\Illuminate\Support\Facades\Route::middleware('api')
|
||||
->group(__DIR__.'/Routes/api.php');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle admin panel booting event.
|
||||
*/
|
||||
|
|
@ -163,6 +182,9 @@ class Boot extends ServiceProvider
|
|||
*/
|
||||
public function onConsole(ConsoleBooting $event): void
|
||||
{
|
||||
// Register middleware alias for CLI context (artisan route:list)
|
||||
$event->middleware('agent.auth', Middleware\AgentApiAuth::class);
|
||||
|
||||
$event->command(Console\Commands\TaskCommand::class);
|
||||
$event->command(Console\Commands\PlanCommand::class);
|
||||
$event->command(Console\Commands\GenerateCommand::class);
|
||||
|
|
|
|||
649
Controllers/AgentApiController.php
Normal file
649
Controllers/AgentApiController.php
Normal file
|
|
@ -0,0 +1,649 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Mod\Agentic\Controllers;
|
||||
|
||||
use Core\Mod\Agentic\Models\AgentPlan;
|
||||
use Core\Mod\Agentic\Models\AgentPhase;
|
||||
use Core\Mod\Agentic\Models\AgentSession;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
|
||||
/**
|
||||
* Agent API Controller.
|
||||
*
|
||||
* REST endpoints consumed by the go-agentic Client (dispatch watch).
|
||||
* All routes are protected by AgentApiAuth middleware with Bearer token.
|
||||
*
|
||||
* Prefix: /api/v1
|
||||
*/
|
||||
class AgentApiController extends Controller
|
||||
{
|
||||
// -------------------------------------------------------------------------
|
||||
// Health
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function health(): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'status' => 'ok',
|
||||
'service' => 'core-agentic',
|
||||
'timestamp' => now()->toIso8601String(),
|
||||
]);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Plans
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* GET /v1/plans
|
||||
*
|
||||
* List plans with optional status filter.
|
||||
* Query params: status, include_archived
|
||||
*/
|
||||
public function listPlans(Request $request): JsonResponse
|
||||
{
|
||||
$workspaceId = $request->attributes->get('workspace_id');
|
||||
|
||||
$query = AgentPlan::where('workspace_id', $workspaceId);
|
||||
|
||||
if ($status = $request->query('status')) {
|
||||
$query->where('status', $status);
|
||||
}
|
||||
|
||||
if (! $request->boolean('include_archived')) {
|
||||
$query->notArchived();
|
||||
}
|
||||
|
||||
$plans = $query->orderByStatus()->latest()->get();
|
||||
|
||||
return response()->json([
|
||||
'plans' => $plans->map(fn (AgentPlan $p) => $this->formatPlan($p)),
|
||||
'total' => $plans->count(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /v1/plans/{slug}
|
||||
*
|
||||
* Get plan detail with phases.
|
||||
*/
|
||||
public function getPlan(Request $request, string $slug): JsonResponse
|
||||
{
|
||||
$workspaceId = $request->attributes->get('workspace_id');
|
||||
|
||||
$plan = AgentPlan::where('workspace_id', $workspaceId)
|
||||
->where('slug', $slug)
|
||||
->first();
|
||||
|
||||
if (! $plan) {
|
||||
return response()->json(['error' => 'not_found', 'message' => 'Plan not found'], 404);
|
||||
}
|
||||
|
||||
return response()->json($this->formatPlanDetail($plan));
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v1/plans
|
||||
*
|
||||
* Create a new plan with optional phases.
|
||||
*/
|
||||
public function createPlan(Request $request): JsonResponse
|
||||
{
|
||||
$workspaceId = $request->attributes->get('workspace_id');
|
||||
|
||||
$validated = $request->validate([
|
||||
'title' => 'required|string|max:255',
|
||||
'slug' => 'nullable|string|max:255',
|
||||
'description' => 'nullable|string',
|
||||
'context' => 'nullable|array',
|
||||
'phases' => 'nullable|array',
|
||||
'phases.*.name' => 'required|string',
|
||||
'phases.*.description' => 'nullable|string',
|
||||
'phases.*.tasks' => 'nullable|array',
|
||||
]);
|
||||
|
||||
$slug = $validated['slug'] ?? AgentPlan::generateSlug($validated['title']);
|
||||
|
||||
$plan = AgentPlan::create([
|
||||
'workspace_id' => $workspaceId,
|
||||
'slug' => $slug,
|
||||
'title' => $validated['title'],
|
||||
'description' => $validated['description'] ?? null,
|
||||
'context' => $validated['context'] ?? null,
|
||||
'status' => AgentPlan::STATUS_DRAFT,
|
||||
]);
|
||||
|
||||
// Create phases if provided
|
||||
$phaseCount = 0;
|
||||
if (! empty($validated['phases'])) {
|
||||
foreach ($validated['phases'] as $order => $phaseData) {
|
||||
$tasks = [];
|
||||
foreach ($phaseData['tasks'] ?? [] as $taskName) {
|
||||
$tasks[] = ['name' => $taskName, 'status' => 'pending'];
|
||||
}
|
||||
|
||||
AgentPhase::create([
|
||||
'agent_plan_id' => $plan->id,
|
||||
'order' => $order,
|
||||
'name' => $phaseData['name'],
|
||||
'description' => $phaseData['description'] ?? null,
|
||||
'tasks' => $tasks ?: null,
|
||||
'status' => AgentPhase::STATUS_PENDING,
|
||||
]);
|
||||
$phaseCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'slug' => $plan->slug,
|
||||
'title' => $plan->title,
|
||||
'status' => $plan->status,
|
||||
'phases' => $phaseCount,
|
||||
], 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH /v1/plans/{slug}
|
||||
*
|
||||
* Update plan status.
|
||||
*/
|
||||
public function updatePlan(Request $request, string $slug): JsonResponse
|
||||
{
|
||||
$workspaceId = $request->attributes->get('workspace_id');
|
||||
|
||||
$plan = AgentPlan::where('workspace_id', $workspaceId)
|
||||
->where('slug', $slug)
|
||||
->first();
|
||||
|
||||
if (! $plan) {
|
||||
return response()->json(['error' => 'not_found', 'message' => 'Plan not found'], 404);
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'status' => 'required|string|in:draft,active,completed,archived',
|
||||
]);
|
||||
|
||||
match ($validated['status']) {
|
||||
'active' => $plan->activate(),
|
||||
'completed' => $plan->complete(),
|
||||
'archived' => $plan->archive(),
|
||||
default => $plan->update(['status' => $validated['status']]),
|
||||
};
|
||||
|
||||
return response()->json([
|
||||
'slug' => $plan->slug,
|
||||
'status' => $plan->fresh()->status,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /v1/plans/{slug}
|
||||
*
|
||||
* Archive a plan with optional reason.
|
||||
*/
|
||||
public function archivePlan(Request $request, string $slug): JsonResponse
|
||||
{
|
||||
$workspaceId = $request->attributes->get('workspace_id');
|
||||
|
||||
$plan = AgentPlan::where('workspace_id', $workspaceId)
|
||||
->where('slug', $slug)
|
||||
->first();
|
||||
|
||||
if (! $plan) {
|
||||
return response()->json(['error' => 'not_found', 'message' => 'Plan not found'], 404);
|
||||
}
|
||||
|
||||
$reason = $request->input('reason');
|
||||
$plan->archive($reason);
|
||||
|
||||
return response()->json([
|
||||
'slug' => $plan->slug,
|
||||
'status' => 'archived',
|
||||
'archived_at' => now()->toIso8601String(),
|
||||
]);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Phases
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* GET /v1/plans/{slug}/phases/{phase}
|
||||
*
|
||||
* Get a phase by order number.
|
||||
*/
|
||||
public function getPhase(Request $request, string $slug, string $phase): JsonResponse
|
||||
{
|
||||
$plan = $this->findPlan($request, $slug);
|
||||
if (! $plan) {
|
||||
return response()->json(['error' => 'not_found', 'message' => 'Plan not found'], 404);
|
||||
}
|
||||
|
||||
$agentPhase = $plan->agentPhases()->where('order', (int) $phase)->first();
|
||||
if (! $agentPhase) {
|
||||
return response()->json(['error' => 'not_found', 'message' => 'Phase not found'], 404);
|
||||
}
|
||||
|
||||
return response()->json($this->formatPhase($agentPhase));
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH /v1/plans/{slug}/phases/{phase}
|
||||
*
|
||||
* Update phase status and/or notes.
|
||||
*/
|
||||
public function updatePhase(Request $request, string $slug, string $phase): JsonResponse
|
||||
{
|
||||
$plan = $this->findPlan($request, $slug);
|
||||
if (! $plan) {
|
||||
return response()->json(['error' => 'not_found', 'message' => 'Plan not found'], 404);
|
||||
}
|
||||
|
||||
$agentPhase = $plan->agentPhases()->where('order', (int) $phase)->first();
|
||||
if (! $agentPhase) {
|
||||
return response()->json(['error' => 'not_found', 'message' => 'Phase not found'], 404);
|
||||
}
|
||||
|
||||
$status = $request->input('status');
|
||||
$notes = $request->input('notes');
|
||||
|
||||
if ($status) {
|
||||
match ($status) {
|
||||
'in_progress' => $agentPhase->start(),
|
||||
'completed' => $agentPhase->complete(),
|
||||
'blocked' => $agentPhase->block($notes),
|
||||
'skipped' => $agentPhase->skip($notes),
|
||||
'pending' => $agentPhase->reset(),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
if ($notes && ! in_array($status, ['blocked', 'skipped'])) {
|
||||
$agentPhase->addCheckpoint($notes);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'slug' => $slug,
|
||||
'phase' => (int) $phase,
|
||||
'status' => $agentPhase->fresh()->status,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v1/plans/{slug}/phases/{phase}/checkpoint
|
||||
*
|
||||
* Add a checkpoint to a phase.
|
||||
*/
|
||||
public function addCheckpoint(Request $request, string $slug, string $phase): JsonResponse
|
||||
{
|
||||
$plan = $this->findPlan($request, $slug);
|
||||
if (! $plan) {
|
||||
return response()->json(['error' => 'not_found', 'message' => 'Plan not found'], 404);
|
||||
}
|
||||
|
||||
$agentPhase = $plan->agentPhases()->where('order', (int) $phase)->first();
|
||||
if (! $agentPhase) {
|
||||
return response()->json(['error' => 'not_found', 'message' => 'Phase not found'], 404);
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'note' => 'required|string',
|
||||
'context' => 'nullable|array',
|
||||
]);
|
||||
|
||||
$agentPhase->addCheckpoint($validated['note'], $validated['context'] ?? []);
|
||||
|
||||
return response()->json([
|
||||
'slug' => $slug,
|
||||
'phase' => (int) $phase,
|
||||
'checkpoints' => count($agentPhase->fresh()->getCheckpoints()),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH /v1/plans/{slug}/phases/{phase}/tasks/{taskIdx}
|
||||
*
|
||||
* Update a task within a phase.
|
||||
*/
|
||||
public function updateTask(Request $request, string $slug, string $phase, int $taskIdx): JsonResponse
|
||||
{
|
||||
$plan = $this->findPlan($request, $slug);
|
||||
if (! $plan) {
|
||||
return response()->json(['error' => 'not_found', 'message' => 'Plan not found'], 404);
|
||||
}
|
||||
|
||||
$agentPhase = $plan->agentPhases()->where('order', (int) $phase)->first();
|
||||
if (! $agentPhase) {
|
||||
return response()->json(['error' => 'not_found', 'message' => 'Phase not found'], 404);
|
||||
}
|
||||
|
||||
$tasks = $agentPhase->tasks ?? [];
|
||||
if (! isset($tasks[$taskIdx])) {
|
||||
return response()->json(['error' => 'not_found', 'message' => 'Task not found'], 404);
|
||||
}
|
||||
|
||||
$status = $request->input('status');
|
||||
$notes = $request->input('notes');
|
||||
|
||||
if (is_string($tasks[$taskIdx])) {
|
||||
$tasks[$taskIdx] = ['name' => $tasks[$taskIdx], 'status' => $status ?? 'pending'];
|
||||
} else {
|
||||
if ($status) {
|
||||
$tasks[$taskIdx]['status'] = $status;
|
||||
}
|
||||
}
|
||||
|
||||
if ($notes) {
|
||||
$tasks[$taskIdx]['notes'] = $notes;
|
||||
}
|
||||
|
||||
$agentPhase->update(['tasks' => $tasks]);
|
||||
|
||||
return response()->json([
|
||||
'slug' => $slug,
|
||||
'phase' => (int) $phase,
|
||||
'task' => $taskIdx,
|
||||
'status' => $tasks[$taskIdx]['status'] ?? 'pending',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v1/plans/{slug}/phases/{phase}/tasks/{taskIdx}/toggle
|
||||
*
|
||||
* Toggle a task between pending and completed.
|
||||
*/
|
||||
public function toggleTask(Request $request, string $slug, string $phase, int $taskIdx): JsonResponse
|
||||
{
|
||||
$plan = $this->findPlan($request, $slug);
|
||||
if (! $plan) {
|
||||
return response()->json(['error' => 'not_found', 'message' => 'Plan not found'], 404);
|
||||
}
|
||||
|
||||
$agentPhase = $plan->agentPhases()->where('order', (int) $phase)->first();
|
||||
if (! $agentPhase) {
|
||||
return response()->json(['error' => 'not_found', 'message' => 'Phase not found'], 404);
|
||||
}
|
||||
|
||||
$tasks = $agentPhase->tasks ?? [];
|
||||
if (! isset($tasks[$taskIdx])) {
|
||||
return response()->json(['error' => 'not_found', 'message' => 'Task not found'], 404);
|
||||
}
|
||||
|
||||
if (is_string($tasks[$taskIdx])) {
|
||||
$tasks[$taskIdx] = ['name' => $tasks[$taskIdx], 'status' => 'completed'];
|
||||
} else {
|
||||
$current = $tasks[$taskIdx]['status'] ?? 'pending';
|
||||
$tasks[$taskIdx]['status'] = $current === 'completed' ? 'pending' : 'completed';
|
||||
}
|
||||
|
||||
$agentPhase->update(['tasks' => $tasks]);
|
||||
|
||||
return response()->json([
|
||||
'slug' => $slug,
|
||||
'phase' => (int) $phase,
|
||||
'task' => $taskIdx,
|
||||
'status' => $tasks[$taskIdx]['status'] ?? 'pending',
|
||||
]);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Sessions
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* GET /v1/sessions
|
||||
*
|
||||
* List sessions with optional filters.
|
||||
* Query params: status, plan_slug, limit
|
||||
*/
|
||||
public function listSessions(Request $request): JsonResponse
|
||||
{
|
||||
$workspaceId = $request->attributes->get('workspace_id');
|
||||
|
||||
$query = AgentSession::where('workspace_id', $workspaceId);
|
||||
|
||||
if ($status = $request->query('status')) {
|
||||
$query->where('status', $status);
|
||||
}
|
||||
|
||||
if ($planSlug = $request->query('plan_slug')) {
|
||||
$plan = AgentPlan::where('workspace_id', $workspaceId)
|
||||
->where('slug', $planSlug)
|
||||
->first();
|
||||
if ($plan) {
|
||||
$query->where('agent_plan_id', $plan->id);
|
||||
} else {
|
||||
return response()->json(['sessions' => [], 'total' => 0]);
|
||||
}
|
||||
}
|
||||
|
||||
$limit = (int) ($request->query('limit') ?: 50);
|
||||
$sessions = $query->latest('started_at')->limit($limit)->get();
|
||||
|
||||
return response()->json([
|
||||
'sessions' => $sessions->map(fn (AgentSession $s) => $this->formatSession($s)),
|
||||
'total' => $sessions->count(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /v1/sessions/{sessionId}
|
||||
*
|
||||
* Get session detail.
|
||||
*/
|
||||
public function getSession(Request $request, string $sessionId): JsonResponse
|
||||
{
|
||||
$workspaceId = $request->attributes->get('workspace_id');
|
||||
|
||||
$session = AgentSession::where('workspace_id', $workspaceId)
|
||||
->where('session_id', $sessionId)
|
||||
->first();
|
||||
|
||||
if (! $session) {
|
||||
return response()->json(['error' => 'not_found', 'message' => 'Session not found'], 404);
|
||||
}
|
||||
|
||||
return response()->json($this->formatSession($session));
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v1/sessions
|
||||
*
|
||||
* Start a new session.
|
||||
*/
|
||||
public function startSession(Request $request): JsonResponse
|
||||
{
|
||||
$workspaceId = $request->attributes->get('workspace_id');
|
||||
$apiKey = $request->attributes->get('agent_api_key');
|
||||
|
||||
$validated = $request->validate([
|
||||
'agent_type' => 'required|string',
|
||||
'plan_slug' => 'nullable|string',
|
||||
'context' => 'nullable|array',
|
||||
]);
|
||||
|
||||
$plan = null;
|
||||
if (! empty($validated['plan_slug'])) {
|
||||
$plan = AgentPlan::where('workspace_id', $workspaceId)
|
||||
->where('slug', $validated['plan_slug'])
|
||||
->first();
|
||||
}
|
||||
|
||||
$session = AgentSession::create([
|
||||
'workspace_id' => $workspaceId,
|
||||
'agent_api_key_id' => $apiKey?->id,
|
||||
'agent_plan_id' => $plan?->id,
|
||||
'session_id' => 'sess_' . \Ramsey\Uuid\Uuid::uuid4()->toString(),
|
||||
'agent_type' => $validated['agent_type'],
|
||||
'status' => AgentSession::STATUS_ACTIVE,
|
||||
'context_summary' => $validated['context'] ?? [],
|
||||
'work_log' => [],
|
||||
'artifacts' => [],
|
||||
'started_at' => now(),
|
||||
'last_active_at' => now(),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'session_id' => $session->session_id,
|
||||
'agent_type' => $session->agent_type,
|
||||
'plan' => $plan?->slug,
|
||||
'status' => $session->status,
|
||||
], 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v1/sessions/{sessionId}/end
|
||||
*
|
||||
* End a session.
|
||||
*/
|
||||
public function endSession(Request $request, string $sessionId): JsonResponse
|
||||
{
|
||||
$workspaceId = $request->attributes->get('workspace_id');
|
||||
|
||||
$session = AgentSession::where('workspace_id', $workspaceId)
|
||||
->where('session_id', $sessionId)
|
||||
->first();
|
||||
|
||||
if (! $session) {
|
||||
return response()->json(['error' => 'not_found', 'message' => 'Session not found'], 404);
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'status' => 'required|string|in:completed,failed',
|
||||
'summary' => 'nullable|string',
|
||||
]);
|
||||
|
||||
$session->end($validated['status'], $validated['summary'] ?? null);
|
||||
|
||||
return response()->json([
|
||||
'session_id' => $session->session_id,
|
||||
'status' => $session->fresh()->status,
|
||||
'duration' => $session->getDurationFormatted(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v1/sessions/{sessionId}/continue
|
||||
*
|
||||
* Continue from a previous session (multi-agent handoff).
|
||||
*/
|
||||
public function continueSession(Request $request, string $sessionId): JsonResponse
|
||||
{
|
||||
$workspaceId = $request->attributes->get('workspace_id');
|
||||
|
||||
$previousSession = AgentSession::where('workspace_id', $workspaceId)
|
||||
->where('session_id', $sessionId)
|
||||
->first();
|
||||
|
||||
if (! $previousSession) {
|
||||
return response()->json(['error' => 'not_found', 'message' => 'Session not found'], 404);
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'agent_type' => 'required|string',
|
||||
]);
|
||||
|
||||
$newSession = $previousSession->createReplaySession($validated['agent_type']);
|
||||
|
||||
return response()->json([
|
||||
'session_id' => $newSession->session_id,
|
||||
'agent_type' => $newSession->agent_type,
|
||||
'plan' => $newSession->plan?->slug,
|
||||
'status' => $newSession->status,
|
||||
'continued_from' => $previousSession->session_id,
|
||||
], 201);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Formatters (match go-agentic JSON contract)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private function formatPlan(AgentPlan $plan): array
|
||||
{
|
||||
$progress = $plan->getProgress();
|
||||
|
||||
return [
|
||||
'slug' => $plan->slug,
|
||||
'title' => $plan->title,
|
||||
'description' => $plan->description,
|
||||
'status' => $plan->status,
|
||||
'current_phase' => $plan->current_phase !== null ? (int) $plan->current_phase : null,
|
||||
'progress' => $progress,
|
||||
'metadata' => $plan->metadata,
|
||||
'created_at' => $plan->created_at?->toIso8601String(),
|
||||
'updated_at' => $plan->updated_at?->toIso8601String(),
|
||||
];
|
||||
}
|
||||
|
||||
private function formatPlanDetail(AgentPlan $plan): array
|
||||
{
|
||||
$data = $this->formatPlan($plan);
|
||||
$data['phases'] = $plan->agentPhases->map(fn (AgentPhase $p) => $this->formatPhase($p))->all();
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function formatPhase(AgentPhase $phase): array
|
||||
{
|
||||
$taskProgress = $phase->getTaskProgress();
|
||||
|
||||
return [
|
||||
'id' => $phase->id,
|
||||
'order' => $phase->order,
|
||||
'name' => $phase->name,
|
||||
'description' => $phase->description,
|
||||
'status' => $phase->status,
|
||||
'tasks' => $phase->tasks,
|
||||
'task_progress' => [
|
||||
'total' => $taskProgress['total'],
|
||||
'completed' => $taskProgress['completed'],
|
||||
'pending' => $taskProgress['remaining'],
|
||||
'percentage' => (int) $taskProgress['percentage'],
|
||||
],
|
||||
'remaining_tasks' => $phase->getRemainingTasks(),
|
||||
'dependencies' => $phase->dependencies,
|
||||
'dependency_blockers' => $phase->checkDependencies(),
|
||||
'can_start' => $phase->canStart(),
|
||||
'checkpoints' => $phase->getCheckpoints(),
|
||||
'started_at' => $phase->started_at?->toIso8601String(),
|
||||
'completed_at' => $phase->completed_at?->toIso8601String(),
|
||||
'metadata' => $phase->metadata,
|
||||
];
|
||||
}
|
||||
|
||||
private function formatSession(AgentSession $session): array
|
||||
{
|
||||
return [
|
||||
'session_id' => $session->session_id,
|
||||
'agent_type' => $session->agent_type,
|
||||
'status' => $session->status,
|
||||
'plan_slug' => $session->plan?->slug,
|
||||
'plan' => $session->plan?->slug,
|
||||
'duration' => $session->getDurationFormatted(),
|
||||
'started_at' => $session->started_at?->toIso8601String(),
|
||||
'last_active_at' => $session->last_active_at?->toIso8601String(),
|
||||
'ended_at' => $session->ended_at?->toIso8601String(),
|
||||
'action_count' => count($session->work_log ?? []),
|
||||
'artifact_count' => count($session->artifacts ?? []),
|
||||
'context_summary' => $session->context_summary,
|
||||
'handoff_notes' => $session->handoff_notes ? ($session->handoff_notes['summary'] ?? '') : null,
|
||||
];
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private function findPlan(Request $request, string $slug): ?AgentPlan
|
||||
{
|
||||
$workspaceId = $request->attributes->get('workspace_id');
|
||||
|
||||
return AgentPlan::where('workspace_id', $workspaceId)
|
||||
->where('slug', $slug)
|
||||
->first();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Core\Mod\Agentic\Controllers\Api\BrainController;
|
||||
use Core\Mod\Agentic\Controllers\Api\PhaseController;
|
||||
use Core\Mod\Agentic\Controllers\Api\PlanController;
|
||||
use Core\Mod\Agentic\Controllers\Api\SessionController;
|
||||
use Core\Mod\Agentic\Controllers\Api\TaskController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agentic API Routes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Brain, Plans, Sessions, Phases, and Tasks endpoints.
|
||||
| Auto-wrapped with 'api' middleware and /api prefix by ApiRoutesRegistering.
|
||||
|
|
||||
*/
|
||||
|
||||
// Health check (no auth required)
|
||||
Route::get('health', fn () => response()->json(['status' => 'ok', 'timestamp' => now()->toIso8601String()]));
|
||||
|
||||
// Authenticated endpoints
|
||||
Route::middleware(['api.auth', 'api.scope.enforce'])->group(function () {
|
||||
|
||||
// Brain (OpenBrain knowledge store)
|
||||
Route::prefix('brain')->name('brain.')->group(function () {
|
||||
Route::post('remember', [BrainController::class, 'remember'])->name('remember');
|
||||
Route::post('recall', [BrainController::class, 'recall'])->name('recall');
|
||||
Route::delete('forget/{id}', [BrainController::class, 'forget'])->name('forget')
|
||||
->where('id', '[0-9a-f-]+');
|
||||
Route::get('list', [BrainController::class, 'list'])->name('list');
|
||||
});
|
||||
|
||||
// Plans
|
||||
Route::prefix('plans')->name('plans.')->group(function () {
|
||||
Route::get('/', [PlanController::class, 'index'])->name('index');
|
||||
Route::get('{slug}', [PlanController::class, 'show'])->name('show');
|
||||
Route::post('/', [PlanController::class, 'store'])->name('store');
|
||||
Route::patch('{slug}', [PlanController::class, 'update'])->name('update');
|
||||
Route::delete('{slug}', [PlanController::class, 'destroy'])->name('destroy');
|
||||
|
||||
// Phases (nested under plans)
|
||||
Route::prefix('{slug}/phases')->name('phases.')->group(function () {
|
||||
Route::get('{phase}', [PhaseController::class, 'show'])->name('show');
|
||||
Route::patch('{phase}', [PhaseController::class, 'update'])->name('update');
|
||||
Route::post('{phase}/checkpoint', [PhaseController::class, 'checkpoint'])->name('checkpoint');
|
||||
|
||||
// Tasks (nested under phases)
|
||||
Route::prefix('{phase}/tasks')->name('tasks.')->group(function () {
|
||||
Route::patch('{index}', [TaskController::class, 'update'])->name('update')
|
||||
->where('index', '[0-9]+');
|
||||
Route::post('{index}/toggle', [TaskController::class, 'toggle'])->name('toggle')
|
||||
->where('index', '[0-9]+');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Sessions
|
||||
Route::prefix('sessions')->name('sessions.')->group(function () {
|
||||
Route::get('/', [SessionController::class, 'index'])->name('index');
|
||||
Route::get('{id}', [SessionController::class, 'show'])->name('show');
|
||||
Route::post('/', [SessionController::class, 'store'])->name('store');
|
||||
Route::post('{id}/end', [SessionController::class, 'end'])->name('end');
|
||||
Route::post('{id}/continue', [SessionController::class, 'continue'])->name('continue');
|
||||
});
|
||||
});
|
||||
Reference in a new issue