Skip to content

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

  1. 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.
  2. 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 _orchestrator desde código nuevo.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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 /chat como 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/.

Comandos de revalidación

rg -l 'from symfonic\.agent\.engine import SymfonicAgent' src/symfonic/agent/fastapi | sort
rg -n 'agent\._(orchestrator|config)|agent\.(resume|resume_interrupt)' src/symfonic/agent/fastapi
rg -n 'create_kernel_agent_router|create_agent_router' examples src/symfonic/cli/templates tests