Mismo criterio que Sprint 4 de Release 2: el primer slice vertical real,
completo (Domain → Base de Datos → Application → Api → pruebas), elegido
para probar que el esqueleto de Sprint 3 (IAiChatClient/IEmbeddingClient,
ADR-0013) funciona de verdad — no otra ronda de diseño en abstracto.
Entre F9 (asistente) y F10 (RAG), F9 es el prerrequisito: el flujo de recuperación de RAG (F10.3) se expone naturalmente como otra herramienta más que el asistente puede invocar ("buscar en mis Documentos") — sin el mecanismo de tool-use ya funcionando, RAG no tiene dónde engancharse. Mismo razonamiento que ya priorizó Catálogos en Release 2: la pieza más simple que prueba el mecanismo central, no la más completa.
- Domain:
AssistantMessage— sin agregadoConversationseparado (ninguna HU pide administrar múltiples hilos con nombre, solo "recordar el contexto de mi conversación actual"); mismo criterio queNotification(Release 2): una entidad simple, sin padre, conTenantId/UserId. - Base de datos: migración
AddAssistantMessages, índice(TenantId, UserId, CreatedAtUtc)— el único patrón de lectura real (cargar el historial propio en orden). Verificada aplicando la cadena completa de migraciones contra una base LocalDB nueva. - Application (
Features/Assistant/):AssistantToolCatalog— el catálogo completo de lo que el modelo puede pedir, hoy con una sola herramienta real (get_my_projects, envuelveGetProjectsQuery).SendAssistantMessageCommand/Handler — el loop de tool-use: persiste el mensaje del usuario, arma el historial + prompt de sistema, llama aIAiChatClient.SendAsync, resuelve cadaAiToolCallRequestinvocando la Query real víaISender(mismo pipeline deAuthorizationBehaviorque cualquier otro caller), vuelve a llamar al modelo con el resultado, hasta obtener texto final (tope de 5 iteraciones). UnForbiddenAccessExceptionde una herramienta se atrapa y se convierte en un mensaje de error legible para el modelo, no en un 403 que tumbe toda la conversación.GetAssistantMessagesQuery/Handler — historial del usuario actual.
- Api:
POST /api/assistant/messages,GET /api/assistant/messages(RequireAuthorization()a nivel de grupo, sin permiso propio — el límite de seguridad vive en cada herramienta individual, no en el endpoint de chat, tal como fija ADR-0013).
SendAssistantMessageCommandHandler ordenaba el historial con
.OrderByDescending(m => m.CreatedAtUtc) dentro de la consulta LINQ —
exactamente el mismo error que GetMyNotificationsQueryHandler cometió en
Release 2/Sprint 9, y que ya había corregido en
GetAssistantMessagesQueryHandler de este mismo Sprint... pero no en el
propio Command handler, que tiene su propia carga de historial
independiente. SQLite (motor de la suite de integración) no traduce ese
ORDER BY; SQL Server sí, así que habría llegado a producción sin que
ningún test lo notara de no ser por esta misma suite. Corregido
materializando primero y ordenando en memoria — misma solución, aplicada
esta vez en dos lugares, no uno.
El mecanismo central — no solo que compila, sino que el modelo
efectivamente no puede ver más de lo que el usuario ya podía ver — se
probó con un doble real, no una respuesta enlatada. FakeAiChatClient
(reemplaza a NullAiChatClient en CustomWebApplicationFactory, sin
claves de API reales disponibles — r3-01-vision-y-alcance.md, sección 0)
ejecuta un loop real de dos idas y vueltas: en la primera llamada pide
la herramienta disponible; en la segunda, ve el resultado real que esa
herramienta devolvió y arma su respuesta a partir de ese texto — no puede
"inventar" una respuesta sin pasar por una herramienta real, porque el
propio fake no tiene otra fuente de la que tomar los datos.
- Respuesta anclada en datos reales: la pregunta "¿qué proyectos tengo?" devuelve una respuesta que contiene el nombre real de un Proyecto sembrado directamente en la base — no un texto genérico.
- Historial persistido correctamente: tras una pregunta, el historial tiene exactamente 2 mensajes (Usuario + Asistente), en el orden correcto.
- Denegación de permiso, sin romper la conversación: un usuario sin
projects.readrecibe una respuesta de texto explicando que no tiene permiso —200 OK, no un403ni un500— y esa respuesta no contiene ningún dato del Proyecto que sí existía. - Aislamiento de tenant real, no solo descrito: un Proyecto sembrado para el tenant A nunca aparece en la respuesta que recibe un usuario del tenant B, aunque ambos hagan exactamente la misma pregunta — la Query que la herramienta invoca ya filtra por tenant (ADR-0003) antes de que el modelo vea el resultado, exactamente lo que ADR-0013 prometía.
- Suite completa: 228/228 tests (132+20+6+70 — 4 nuevas de integración,
5 nuevas de Domain),
dotnet format --verify-no-changeslimpio. - Sin verificación real de OpenAI/Anthropic — mismo límite ya declarado en Sprint 3: sin implementaciones concretas todavía (llegan en el Sprint de Backend) y sin claves de API disponibles en este entorno de todos modos.
- RAG (F10) — ninguna entidad ni handler nuevo; depende del mecanismo de tool-use que este Sprint recién valida, se construye en un Sprint posterior con su propia herramienta ("buscar en mis Documentos").
- Más de una herramienta real —
get_my_projectses suficiente para probar el mecanismo; agregarget_my_tasks/get_my_documents/etc. es trabajo del Sprint de Backend, no de Validación. - Streaming de la respuesta — ya diferido explícitamente en Sprint 1.
- Cualquier endpoint o componente de frontend — Sprint de Frontend.