Arquitectura
Estructura de carpetas
Sección titulada «Estructura de carpetas»emaemi/├── src/│ ├── index.ts # Hono app + CORS│ ├── env.d.ts # Cloudflare bindings (secrets)│ ├── api/│ │ └── chat.ts # POST /api/chat│ ├── orchestrator/│ │ └── router.ts # AppName → AgentName│ ├── agents/│ │ ├── base.ts # BaseAgent abstracto, tool loop│ │ ├── sales.ts # SalesAgent (sin tools)│ │ ├── support.ts # SupportAgent (11 tools FHIR)│ │ └── analytics.ts # AnalyticsAgent (6 tools Supabase)│ ├── middleware/│ │ └── auth.ts # Resolver de API key + tenant validator│ ├── lib/│ │ ├── claude.ts # fetch nativo + retry 529 + tool loop│ │ ├── fhir-client.ts # Query builder Aidbox│ │ ├── supabase-client.ts # Query builder Supabase│ │ ├── permissions.ts # APP_SCOPES (qué agente y tools por app)│ │ └── types.ts # AppName, AgentName, ChatRequest, etc.│ └── tools/│ ├── clinic.ts # 11 herramientas FHIR│ └── vault.ts # 6 herramientas Supabase└── tests/Flujo de una request
Sección titulada «Flujo de una request»[Cliente: emahealth / emaclinic / emavault] ↓ POST /api/chat ↓ Authorization: Bearer EMI_API_KEY ↓ X-Tenant-Id (sólo emaclinic) ↓ Body: { messages, context } ↓[authMiddleware] ├─ valida API key → resuelve AppName (emahealth | emaclinic | emavault) └─ valida X-Tenant-Id si applica ↓[Router.getAgent(app)] └─ retorna instancia singleton del agente ↓[agent.run(messages, context)] ├─ buildSystemPrompt(locale, turnCount, tenantId, patientId, encounterId) ├─ runAgent() → invoca Claude API └─ tool_use loop (MAX_TOOL_ITERATIONS = 5) ├─ Claude pide tool ├─ ToolExecutor ejecuta función (FHIR / Supabase) ├─ inserta tool_result └─ vuelve a Claude hasta `end_turn` ↓[Response: { message, agent, turnCount }]Mapeo App → Agent
Sección titulada «Mapeo App → Agent»Tabla en permissions.ts → APP_SCOPES:
| AppName | Agente | Tools | Tenant requerido |
|---|---|---|---|
emahealth | SalesAgent | ninguna (Q&A puro) | no |
emaclinic | SupportAgent | 11 FHIR (Aidbox) | sí (X-Tenant-Id) |
emavault | AnalyticsAgent | 6 Supabase | no |
Patrones de routing
Sección titulada «Patrones de routing»- Simple (Phase 1): AppName → AgentName fijo (1:1).
- Scoped: cada app sólo puede invocar agentes/tools que están en su whitelist.
- Tenant-aware: emaclinic obliga
X-Tenant-Iden el header. Las queries FHIR se scope-an automáticamente al tenant; sin header → 400.
Sistema de prompts
Sección titulada «Sistema de prompts»Los system prompts viven hardcodeados en cada agente (SYSTEM_PROMPT const).
La función buildSystemPrompt() inyecta contexto dinámico:
locale(ej.es,en)turnCount(cuántos mensajes lleva el usuario)tenantId,patientId,encounterId(cuando aplican)
No hay versionado ni hot-swap: cambiar un prompt requiere redeploy. Phase 3 propone mover los prompts a un store versionado.
Persistencia
Sección titulada «Persistencia»No persiste conversaciones. Cada request es stateless: el historial viene en el body de la request (lo administra el cliente).
Phase 3 contempla persistir conversaciones en una BD para auditoría y context windowing inteligente.