Categoría: Conector + SoR pequeño propio (Plan Store + executed_plan_history) Estado: Conector hipertrofiado por defecto en V3 — la cicatriz Holded vive aquí, contenida. Síntesis de: Claude I1 + Codex I1
I1 es el componente que convierte una intención contable de Intelia (Internal Plan del Motor) en una operación segura, trazable y verificable dentro de Holded.
Toda la cicatriz Holded vive aquí. Cero menciones a Holded fuera de I1.
I2 paralelo a I1, no extensión.holded_integration/
├── snapshot_reader/ # produce facts/erp_snapshot/ para M1
├── plan_generator/ # agente LLM con 3 roles internos + guías + tool wrapper
│ ├── agent.py # Planner + Tool Mapper + Critic en un único loop
│ ├── guides/ # markdown editable, SIN razonamiento semántico
│ └── tool_wrapper.py # HoldedAdapter read-only para que el agente consulte estado
├── plan_store/ # reglas calcificadas DE INTEGRACIÓN Holded (no semánticas)
├── executor/ # Temporal-durable, dumb, sin razonamiento
├── final_validation/ # PlanState vs readback de Holded con comparators declarativos
├── error_classifier/ # técnicos / operativos / semánticos
└── adapter/ # HTTP/auth/retry primitives
├── holded_internal.py
└── holded_public.py
El Plan Generator de Holded se implementa conceptualmente como un agente LLM único con tres roles internos:
No se separa en sub-agentes físicos desde el día uno. Roles internos permiten prompts y evaluaciones diferenciadas sin multiplicar infraestructura. Si en producción aparecen errores sistemáticos de tool mapping o crítica débil, se extraen entonces.
(Internal Plan) + erp_context_snapshot reciente.plan_store por (intent_signature, erp_context_signature). Hit → return ExecutionPlan cached.ExecutionPlan estructurado.served_by: rule_<id> o agent_v<X> + confidence + reasoning_trace.Las guías son markdown editable por Intelia ops. Política dura: las guías contienen cómo se hace en Holded, nunca qué es semánticamente.
| ❌ Mal | ✅ Bien |
|---|---|
"Si la factura es un ISP, hacer PUT con tax_key: s_iva_isp" |
"Cuando el Internal Plan declara vat_application_mechanism: REVERSE_CHARGE, usar tax_key: s_iva_isp" |
| "Si el documento parece rectificativa, hacer DELETE+CREATE" | "Cuando el Internal Plan tiene replaces_original: True, ejecutar secuencia delete_payments → delete_doc → create_doc → restore_payments" |
El agente NO debe inferir si algo es ISP — lo declara el Motor. Si la guía obliga al agente a razonar "esto parece ISP porque...", el Motor está incompleto.
Declarativo pero específico de Holded. Gramática pequeña y versionada, no DSL libre:
ExecutionPlan {
objective: "create_purchase" | "update_purchase" | "void" | "amend" | ...,
preconditions: list[Precondition], # qué debe ser cierto en el SoR
operations: list[Operation], # ordered, declaradas
plan_state_declared: PlanState, # qué se espera en readback
traceability: { # link al plan + tags + comment
intelia_plan_id: UUID,
tag: "intelia-plan-id-<uuid>",
comment_template: "[Intelia] ..."
},
idempotency_strategy: Strategy,
fallback_on_error: "block" | "escalate", # NO "silent_fallback"
explanation: str, # razonamiento del agente
confidence: float,
served_by: "rule_R-xxx" | "agent_v3.4"
}
Sin fallback silencioso. Si una operación no se puede ejecutar, el plan se bloquea o escala — nunca se "arregla" con una decisión inventada por el executor.
Temporal-durable. No razona. Recibe ExecutionPlan validado y lo ejecuta.
ExecutorActivity {
input: ExecutionPlan,
output: ExecutionResult {
executed_operations: [...],
skipped: [...],
failed_at: index?,
observed_state: dict (readback),
compensations_applied: [...]
}
}
Reglas:
dedupe_key en cada operación.Compara PlanState declarado vs observed_state (readback de Holded post-ejecución).
Output: PASSED | PASSED_WITH_WARNINGS | FAILED | COMPENSATION_REQUIRED.
Los comparator_overrides y el provider drift profile son declarativos y vivien en final_validation/. Es deuda Holded explícita y versionada.
I1 distingue tres tipos:
| Tipo | Ejemplo | Acción |
|---|---|---|
| Técnico | Timeout, 5xx de Holded | Retry automático según política Temporal |
| Operativo | Account no existe, contact no existe, lock activado | Bloquea, emite signal de remediación, deriva a Pending Plans |
| Semántico | Internal Plan implica cuenta X pero Holded requiere Y | Devuelve conflicto al Motor + S1, no corrige silenciosamente |
Produce facts/erp_snapshot/ para que el Motor lo consuma como input:
Cada loader tiene política de refresh (lazy / on-rejection / scheduled) y TTL explícito. El Motor NO conoce estos loaders — recibe el snapshot ya empaquetado.
Schema:
PlanRule {
rule_id: UUID,
integrator: "holded",
rule_type: "execution_plan",
intent_signature: str,
erp_context_signature: str,
execution_plan_template: ExecutionPlan,
confidence: float,
lifecycle_state: "candidate" | "shadow" | "active" | "quarantined" | "retired",
promoted_from: "shadow" | "bootstrap" | "manual",
bootstrap_origin: {client_id, sector}?,
guide_versions: dict[str, str],
invalidation_signals: [...],
served_count: int,
last_served_at: datetime
}
Lookup: match exacto dado (intent_signature, erp_context_signature). Sin fuzzy. La fuzziness vive en la signature (bucket de magnitudes, normalización), no en el lookup.
Invalidación:
guide_version cambia o detecta criterion drift.Las reglas inactivas NO se borran. Quedan con lifecycle_state: retired para auditoría.
La frontera es el Internal Plan. Si I1 necesita cambiar cuenta, impuesto o criterio contable para poder ejecutar, devuelve conflicto a M1/S1, no corrige silenciosamente.
El Plan Store de I1 es el repositorio de reglas de traducción. T3 gestiona el lifecycle, pero I1 posee el contenido y las features de matching.
I1 debe dejar trazabilidad suficiente en Holded para que T4 pueda asociar cambios posteriores:
intelia-plan-id-<uuid> único por documento;[Intelia] + Plan ID + URL;Sin trazabilidad insertada por I1, T4 no puede saber qué cambios son del contable vs de Intelia.
| Riesgo | Mitigación |
|---|---|
| El agente Plan Generator empieza a decidir semantics | Code review obligatoria de guías. Lint que busca verbs semánticos en guías ("decidir", "evaluar", "interpretar") |
| El Plan Store guarda planes con shape variable que rompen comparación | ExecutionPlan tiene schema Pydantic estricto. Planes con shape distinto no son calcificables |
| Reglas calcificadas obsoletas tras cambio de Holded API | guide_version por regla + invalidación automática cuando se actualiza guía. Período de shadow obligatorio post-invalidación |
| Drift profile crece a 10k entries ingobernable | Owner explícito por entry. Retention policy. Audit cuatrimestral |
| Latencia del Plan Generator destruye UX en cold start | Plan Store como protección. Cold start solo en cliente nuevo / patrón nuevo. Bootstrap cruzado reduce cold a primeras semanas |
| Concurrencia entre Intelia escribiendo y contable editando | Optimistic lock por last_modified_at. Si conflict → Final Validation FAIL → revisión humana |
Patología 1: I1 se convierte en "M1 con APIs". Ocurre si los errores de M1 se arreglan en Holded porque "es más rápido". Eso destruye auditabilidad: ya no se sabe quién decidió qué. Mitigación: todo cambio semántico requiere nuevo Internal Plan o aprobación humana explícita.
Patología 2: Plan Generator se enamora del razonamiento. El LLM genera planes brillantes con reasoning_trace extenso, pero el equipo deja de calcificar. 95% de docs siguen llamando al agente. Mitigación: KPI obligatorio %_served_by_rule. Si <30% a los 3 meses, alarma + revisión de T3.
Patología 3: las guías acaban siendo prompts de 50 páginas. Cada workaround Holded entra como párrafo nuevo. A los 18 meses, ilegible. Mitigación: tamaño bounded por guía (max 500 líneas). Si excede, dividir por subdominio.
Patología 4: drift profile se vuelve la nueva cicatriz. Cada vez que Holded da un readback distinto, alguien añade entrada en lugar de arreglar la causa. Mitigación: cada entry con justification + owner. Audit ratio drift_profile_size / total_writes.
Patología 5: el agente lee Holded constantemente vía tool y satura. Con 1000 docs/día, 3-5 read calls/doc = 5000 calls/día. Mitigación: cache local agresivo en tool_wrapper. Lecturas frecuentes con TTL corto.
| Métrica | Target |
|---|---|
i1_plan_store_hit_rate_per_client |
Crece a >70% a los 6 meses |
i1_plan_generator_llm_calls_per_doc |
Decrece con calcificación; baseline 1.0 → target 0.2 |
i1_executor_latency_p95 |
<10s plans calcificados, <30s con agente |
i1_final_validation_failed_rate |
<2% |
i1_compensation_triggered_per_month |
Bajo. Cada uno requiere audit individual |
holded_tool_calls_per_doc |
<5 (con cache eficiente) |
intent_signature? Subset del Intent que determina la regla. Trade-off granularidad.executor.activities.py?Síntesis. Detalle granular en versiones originales.