I1 — Integrador Holded

I1 — Integrador Holded

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


1. Responsabilidad

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.

2. NO es su responsabilidad

  • No decide si una factura va a 623, 629 o 600. Eso es decisión de M1.
  • No decide deducibilidad fiscal. Eso es M1.
  • No persiste libro diario. Holded es el SoR. I1 persiste su propio SoR pequeño (Plan Store + executed_plan_history) que NO compite con el ledger Holded.
  • No habla con otros ERPs. Cuando llegue Sage, será I2 paralelo a I1, no extensión.
  • No interpreta sustancia económica, salvo para comprobar que la traducción respeta el Internal Plan.

3. Sub-componentes

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

4. El Plan Generator — agente único con 3 roles internos

El Plan Generator de Holded se implementa conceptualmente como un agente LLM único con tres roles internos:

  • Planner: traduce Internal Plan a intención Holded.
  • Tool Mapper: selecciona operaciones y datos concretos de Holded (consulta tool wrapper si necesita leer estado).
  • Critic: verifica precondiciones, idempotencia y expected state.

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.

Flujo

  1. Recibe del Motor: (Internal Plan) + erp_context_snapshot reciente.
  2. Lookup en plan_store por (intent_signature, erp_context_signature). Hit → return ExecutionPlan cached.
  3. Miss → llama al agente con prompt sistema + guías Holded relevantes + Internal Plan + acceso read-only al tool wrapper.
  4. El agente razona, hace read calls al ERP via tool si necesita, produce un ExecutionPlan estructurado.
  5. Marca served_by: rule_<id> o agent_v<X> + confidence + reasoning_trace.

Las guías Holded

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.

5. El ExecutionPlan Holded — gramática conceptual

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.

6. El Executor — determinístico, dumb

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:

  • Cada operación es una activity Temporal o varias agrupadas según dominio de fallo.
  • Idempotencia por dedupe_key en cada operación.
  • Compensación: si falla en step N, revierte 1..N-1 cuando es posible.
  • El Executor no decide. Si algo se desvía, bloquea o escala. Nunca "interpreta" un fallo.

7. Final Validation

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.

8. Clasificación de errores

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

9. Snapshot Reader

Produce facts/erp_snapshot/ para que el Motor lo consuma como input:

  • chart of accounts;
  • period locks;
  • ledger histórico relacionado;
  • payments existentes;
  • tags y proyectos;
  • duplicados candidatos.

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.

10. Plan Store

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:

  • Por T3 cuando una regla recibe muchas correcciones.
  • Por T4 cuando guide_version cambia o detecta criterion drift.
  • Manual desde dashboard ops.

Las reglas inactivas NO se borran. Quedan con lifecycle_state: retired para auditoría.

11. Frontera con M1

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.

12. Frontera con T3

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.

13. Frontera con T4

I1 debe dejar trazabilidad suficiente en Holded para que T4 pueda asociar cambios posteriores:

  • tag intelia-plan-id-<uuid> único por documento;
  • comment estructurado con prefijo [Intelia] + Plan ID + URL;
  • (opcional) custom field si Holded lo soporta cleanly.

Sin trazabilidad insertada por I1, T4 no puede saber qué cambios son del contable vs de Intelia.

14. Riesgos arquitectónicos y mitigaciones

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

15. Lo que puede salir mal

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.

16. Métricas operacionales

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)

17. Open questions

  • ¿Cuál es la gramática mínima de ExecutionPlan Holded para cubrir facturas, gastos, asientos, contactos, proyectos y tags sin volverse lenguaje general?
  • ¿Schema del intent_signature? Subset del Intent que determina la regla. Trade-off granularidad.
  • ¿Plan Generator es agente Temporal-aware o synchronous? Trade-off durabilidad vs latencia.
  • ¿Las guías se versionan en git o en DB? Trade-off audit vs edición desde dashboard.
  • ¿Política de "qué hacer cuando el agente declara confidence baja sobre un caso edge"? Threshold + tipos de caso (e.g. ISP) que siempre van a humano hasta calcificar.
  • ¿Holded como surface (link + tag) merece su propio sub-componente o se queda en executor.activities.py?

Síntesis. Detalle granular en versiones originales.