import { Controller, Post, Body, Logger, HttpException, HttpStatus, NotFoundException, UseGuards, } from '@nestjs/common'; import { Throttle } from '@nestjs/throttler'; import { AuthGuard } from '../auth/auth.guard.js'; import { CurrentUser } from '../auth/current-user.decorator.js'; import { scopeFromUser, type AuthenticatedUserLike } from '../auth/session-scope.js'; import { v4 as uuid } from 'uuid'; import { ChatRequestDto } from './chat.dto.js'; import { ChatRuntimeRouter } from './chat-runtime-router.js'; import { ownConversation } from './chat-runtime.js'; import type { LegacyRuntimeFailure } from './chat-runtime.js'; interface ChatResponse { conversationId: string; text: string; } @Controller('api/chat') @UseGuards(AuthGuard) export class ChatController { private readonly logger = new Logger(ChatController.name); constructor(private readonly runtime: ChatRuntimeRouter) {} @Post() @Throttle({ default: { limit: 10, ttl: 60_000 } }) async chat( @Body() body: ChatRequestDto, @CurrentUser() user: AuthenticatedUserLike, ): Promise { const conversationId = body.conversationId ?? uuid(); const scope = scopeFromUser(user); this.logger.debug(`Handling chat request for user=${user.id}, conversation=${conversationId}`); // The one exclusive runtime owns execution. In legacy mode this reaches the embedded runtime; // in pi-rpc it fails closed with `runtime_unsupported` before ever touching embedded execution. const result = await this.runtime.completeLegacyRestTurn( ownConversation(conversationId, scope), { content: body.content }, ); if (result.ok) { return { conversationId, text: result.value.text }; } throw this.toHttpException(result, conversationId); } /** Maps a total {@link LegacyRuntimeFailure} to the fixed browser-safe HTTP surface. */ private toHttpException(failure: LegacyRuntimeFailure, conversationId: string): HttpException { switch (failure.code) { case 'conversation_unavailable': return new NotFoundException('Session not found'); case 'request_invalid': case 'thinking_level_invalid': return new HttpException('Invalid chat request', HttpStatus.BAD_REQUEST); case 'timeout': return new HttpException('Agent response timed out', HttpStatus.GATEWAY_TIMEOUT); case 'runtime_unsupported': case 'runtime_unavailable': return new HttpException('Agent runtime unavailable', HttpStatus.SERVICE_UNAVAILABLE); default: this.logger.error(`Chat turn failed for conversation=${conversationId}: ${failure.code}`); return new HttpException('Agent processing failed', HttpStatus.INTERNAL_SERVER_ERROR); } } }