CORE-23 — Migración de composition roots FastAPI fuera de engine.py¶
Decisión de alcance¶
Este es un inventario de migración, no una autorización para cambiar routers ni
contratos HTTP. La composición objetivo es FastAPI ->
create_kernel_agent_router(AgentHost) -> Agent, no FastAPI ->
create_agent_router(SymfonicAgent). La evidencia es el código actual; las
anotaciones y los comentarios no se toman como prueba de que una ruta esté en
producción.
El root kernel existe y se usa en el ejemplo distribuido
(examples/agent/fastapi_app.py:compose, app.include_router) y en el
scaffold (src/symfonic/cli/templates/app/main.py.j2:create_app). Ambos
construyen un AgentHost, componen Agent por scope y montan
symfonic.platform.transport.create_kernel_agent_router.
La compatibilidad viva es src/symfonic/agent/fastapi/router.py:create_agent_router.
Recibe un SymfonicAgent; sus rutas de turno llaman run, stream,
stream_typed, resume y resume_interrupt. Por tanto, aunque un turno
admitido pueda llegar al Kernel a través del dispatcher de compatibilidad, este
root sigue siendo Dual-path, no kernel-native.
Conteo reconciliado¶
| Medición | Resultado actual | Método y límite |
|---|---|---|
Archivos bajo src/symfonic/agent/fastapi/ con from symfonic.agent.engine import SymfonicAgent |
8 | rg -l de ese import. Todos están bajo TYPE_CHECKING; es una dependencia de tipo, no una importación runtime por sí sola. |
| Raíces FastAPI legacy que construyen/reciben el agente | 1 pública | create_agent_router recibe el objeto en router.py:create_agent_router; monta los cuatro subrouters. |
Archivos de src/ con import sintáctico de engine.py |
24, incluido engine.py |
El mismo rg devuelve 23 consumidores externos. Este número no equivale a hot paths. |
| Reclamo previo de “8 consumidores FastAPI” | Confirmado con precisión | Son ocho módulos, pero una sola raíz pública y siete apoyos/subrouters; no ocho aplicaciones independientes. |
La auditoría anterior citó 22 módulos src/ consumidores. El inventario actual
no debe ocultar la discrepancia: el patrón exacto devuelve 23 consumidores
externos, o 24 si se incluye el propio engine.py. La diferencia sólo se puede
atribuir a cambios desde ese conteo o a su filtro de exclusiones; se debe volver
a ejecutar el comando al abrir el PR de CORE-23, no declarar regresión o mejora
por el número histórico.
Raíces y contratos de runtime¶
| Root | Construcción/agente efectivo | Contrato que hoy posee | Objetivo | Riesgo de admisión/capability |
|---|---|---|---|---|
agent.fastapi.router:create_agent_router |
Recibe SymfonicAgent |
API /api/v1; auth gate, codec, mapeo de errores, SSE y administración legacy |
Adaptador de compatibilidad temporal; descomponer por puertos antes de reemplazarlo | Lee _orchestrator y _config; no puede aceptar Agent como sustitución mecánica. |
platform.transport:create_kernel_agent_router |
host.agent_for(scope) devuelve Agent |
Scope autenticado, host cerrado=503, request/history/session validation, /chat, /stream, /stream/typed; data/record routes opcionales |
Root canónico | No implementa /resume; su JSON/SSE no es todavía paridad wire con el router legacy. |
examples/agent/fastapi_app.py:compose |
AgentHost(composer=compose), Agent |
Ejemplo empaquetado single-tenant | Referencia mínima de root canónico | No prueba memoria, graph, continuación ni contratos legacy. |
cli/templates/app/main.py.j2:create_app |
_build_agent() devuelve AgentHost |
Scaffold FastAPI, memory/graph admin y resolver autenticado | Referencia productizable | Debe ser el primer consumidor que se pruebe por generación real. |
Ocho módulos legacy y por qué no se pueden sustituir aún¶
| Módulo | Evidencia de uso efectivo | Rutas/contrato | Puerto objetivo antes de migrar |
|---|---|---|---|
router.py |
create_agent_router; agent.run/stream/stream_typed y continuación |
Turnos, SSE, resume, chats/memories/procedures/consolidation; incluye los subrouters | AgentHost + transport de turnos; puertos de memoria/procedimientos/consolidación/continuación. |
memory_create_router.py |
create_memory_create_router incluido por router.py |
POST semantic, episodic, procedural, working, prospective | MemoryAdminService o puerto de escritura explícito, scoped por principal. |
memory_create_handlers.py |
handlers llamados por el subrouter | Validación y escritura por capa | Mismo puerto de escritura; conservar código/status/audit. |
memory_create_helpers.py |
require_layer usa agent._orchestrator.get_layer |
Resolución de layers | Eliminar la introspección mediante una interfaz de storage/administración. |
graph_router.py |
create_graph_router incluido por router.py |
Neighborhood, paths, edges, clusters | GraphAdminService; el root kernel ya acepta graph=. |
graph_maintenance_router.py |
create_graph_maintenance_router incluido por router.py |
Bulk delete, audit, prune | Servicio de graph maintenance scoped, no _orchestrator. |
tenant_privacy_router.py |
create_tenant_privacy_router incluido por router.py |
Export y borrado GDPR | platform.privacy / servicio de privacidad con principal verificado. |
tenant_privacy_export.py |
helpers de export llamados por privacy router | Export traversa agent._orchestrator |
Servicio de export sobre stores explícitos. |
Los imports de engine.py en los ocho módulos son TYPE_CHECKING, pero no son
inofensivos: los contratos de ejecución reales requieren la forma privada de
SymfonicAgent (_orchestrator, _config, metrics_collector) en
router.py, helpers de memoria, graph y privacy. Cambiar sólo la anotación a
Agent produciría una falsa migración.
Inventario de rutas legacy que requieren prueba¶
| Familia | Evidencia | Prueba de salida requerida antes del cambio |
|---|---|---|
| HTTP de turno | router.py:chat |
Código/status, respuesta AgentResponse, scope/principal ligado, budget gate y mapeo de errores. |
| SSE texto | router.py:stream |
Orden de frames, cancelación/desconexión, citaciones y error terminal; comparar wire, no sólo texto. |
| SSE typed | router.py:stream_typed |
Clase de evento, payload JSON y orden de tool/text/error. El root kernel actual deriva de agent.stream, no de un API typed separado. |
| Resume | router.py:resume |
Token firmado/expirado/scope, schema por interrupt, ask_user y resume_interrupt, SSE de continuación. Bloqueador: platform.transport declara explícitamente que resume no está implementado. |
| Memory CRUD / procedures | router.py:list_memories hasta toggle_procedure |
Tenant isolation, 404/400/403, mutación y auditoría; extraer puertos antes de mover handlers. |
| Memory create | memory_create_router.py |
Cinco layers, validación, persistencia, status 201 y aislamiento. |
| Graph | graph_router.py, graph_maintenance_router.py |
Lectura, creación/borrado, bulk delete, audit/prune, autorización y store correcto. |
| Privacy | tenant_privacy_router.py |
Export completo y borrado irreversible por tenant, auditoría y no cross-tenant leak. |
| Consolidation | router.py:consolidate_memory |
Parámetros legacy de config y efectos en semantic/episodic/procedural. No asumir equivalencia por respuesta 200. |
| Métricas | inclusión condicional en router.py por metrics_collector |
Opt-in, shape y fuente durable; trasladar a servicios, no a atributos del agente. |
Orden seguro de migración¶
- Cerrar contratos del root nativo. Añadir pruebas de aplicación generada y
del ejemplo empaquetado para
/chat, ambos SSE, scope, history y cierre de host. No cambiar el router legacy. - Extraer/montar rutas que ya tienen servicios kernel. Migrar graph y
lectura/administración de memoria a
GraphAdminService/MemoryAdminService; prueba de equivalencia HTTP y de aislamiento por cada grupo. No acceder a_orchestratordesde código nuevo. - Crear puertos faltantes. Privacy export/erase, memory create, procedures, consolidation y métricas necesitan servicios explícitos. Cada puerto debe declarar scope/principal, error y audit; sólo después se mueven handlers.
- Resolver continuación como contrato separado. Diseñar y probar el port de continuations en Kernel/host. No retirar el endpoint legacy ni publicar 501 como “migración” mientras consumidores requieran resume.
- Comparación diferencial de roots. Para cada contrato admitido, ejecutar el mismo request contra legacy y kernel-native, comparar status, headers, media type, frames y efectos persistidos. Registrar fallback/admisión.
- Cambiar el scaffold/ejemplos que aún apunten a compatibilidad, después consumidores producto. El scaffold ya es nativo; preservar esa aserción. Mantener el router legacy como adaptador hasta que no tenga rutas exclusivas ni consumidores públicos.
- Gate de eliminación. Exigir cero imports runtime de engine desde FastAPI, cero acceso a privados legacy, telemetría durable de fallback cero durante la ventana acordada y aprobación de las filas del ledger. Sólo entonces despublicar y retirar.
Pruebas y mediciones a crear en CORE-23 / CORE-21¶
- Un harness ASGI común que acepte un request, ejecute ambos roots cuando el contrato esté disponible y normalice únicamente campos no deterministas.
- Pruebas de HTTP, stream y typed stream separadas: no usar una respuesta de
/chatcomo evidencia de SSE. - Casos de scope/auth: sin tenant=401, principal inválido, tenant ajeno, host cerrado=503 y no contaminación entre tenants.
- Casos de persistencia: memory create/read, graph mutation, privacy export / erase y consolidation, con stores por tenant.
- Casos de resume: hasta que el root nativo exista, registrar la ausencia como
UNKNOWN/BLOCKED, no como paridad aprobada. - Métricas por request: root (
legacy|kernel), decisión de admisión, fallback, capability set, contrato y resultado HTTP/SSE. No incluir prompts, tokens de autorización ni PII.
Criterio de terminación de CORE-23¶
CORE-23 está listo para implementar, no para cerrar, cuando cada fila anterior
tenga dueño, puerto objetivo, test diferencial y decisión de compatibilidad.
Se cierra únicamente cuando el root kernel sirve todos los contratos que se
declaren soportados, los exclusivos legacy estén deprecados con decisión
aprobada, y el grep de imports runtime/accesos privados llegue a cero bajo
src/symfonic/agent/fastapi/.