Categoría: Plumería transversal (no contiene lógica de negocio) Estado: Propio sano — mantenido del target V2 documentado. Síntesis de: Claude TX + Codex TX
TX es la capa transversal que hace que V3 sea durable, observable, idempotente y aprendible. Coordina procesos, persiste señales operacionales y mantiene la historia de cada documento / plan.
| Sub-componente | Función |
|---|---|
| TX.Signals | Bus inmutable tipado que acumula eventos de las capas y los proyecta a superficies (tags ERP, comments, audit log, alerts internas). |
| TX.Temporal | Durabilidad, retries por dominio de fallo, idempotencia por workflow_id, aislamiento de entornos. |
AccountingV3Pipeline directo, saltándose Temporal.V3 necesita eventos estructurados para poder aprender y auditar.
Cada signal tiene SignalSpec:
SignalSpec {
code: str, # "DUDA_CUENTA", "EDITED_BY_HUMAN", ...
severity: ERROR | WARNING | INFO,
intent: REVIEW | INFORM | BLOCK,
audience: list[INTERNAL | CLIENT_VIA_ERP_TAG | CLIENT_VIA_ERP_COMMENT | AUDIT_LOG],
template_message_es: str, # i18n string
blocks_execution: bool,
dedupe_key_template: str?,
}
Emitir un signal con código no registrado falla. Tipado runtime.
| Familia | Ejemplos | Audience típica |
|---|---|---|
| Decisión IA con duda | DUDA_CUENTA, DUDA_IMPUTACION, DUDA_DEDUCIBILIDAD |
tag ERP + comment + audit |
| Decisión IA con fallback | AVISO_CUENTA_GENERICA, AVISO_IMPUTACION_GENERICA |
tag ERP + audit |
| Error material | ERROR_LINEAS_VS_TOTAL, ERROR_CUENTA_INVALIDA |
tag ERP + comment + alert |
| Provider info | PROVEEDOR_NUEVO |
tag ERP + audit |
| Calcificación | REGLA_APLICADA, REGLA_PROPUESTA_PROMOVIDA, REGLA_INVALIDADA |
audit + alert ops |
| Reverse-sync | EDITED_BY_HUMAN, CRITERION_CHANGED, APPROVED_BY_HUMAN |
audit + alert ops |
| Validación post-write | READBACK_DRIFT, COMPENSATION_REQUIRED |
audit + alert |
| UX | PENDING_PLAN_CREATED, BULK_APPROVE_DETECTED |
audit |
Cada signal tiene dedupe_key (template parametrizado por document_id, plan_id, etc.). El bus mantiene seen_dedupe_keys por workflow execution. Mismo key → segundo emit se descarta.
Al final del workflow, signal_projector consume el SignalBag y proyecta:
| Surface | Cómo |
|---|---|
| Tags ERP | holded.add_tags(document, [signal.code para signal con CLIENT_VIA_ERP_TAG audience]) |
| Comments ERP | holded.post_comment(document, render_signals_as_md([...]) con dedupe por hash |
| Audit log Intelia | INSERT en signal_audit_log table |
| Alerts internas | Slack/PostHog para signals ERROR o patrón excede umbral |
La projection es idempotente. Re-ejecución no duplica.
Parent workflow por documento:
DocumentAccountingWorkflow {
children: [
MotorWorkflow (M1 fases 1-5),
IntegratorWorkflow (I1: plan gen + validation + executor + final val)
],
signals: emite/recibe del bus,
output: ExecutionOutcome
}
Independiente, continuous (con continue_as_new periódico para liberar history):
ReverseSyncMonitorWorkflow {
activities: [
poll_holded_changes_activity,
interpret_comments_activity (LLM),
emit_signals_to_bus_activity
]
}
Scheduled, durable:
CalcificationLoopWorkflow {
activities: [
collect_evidence_activity,
propose_rule_activity,
shadow_test_activity (compara contra agente N=15),
wait_for_intelia_ops_approval (signal-based),
promote_rule_activity
]
}
workflow_id determinista por document_id + version.workflow_id = f"doc-{document_id}-v{m1_version}". Nuevo workflow_id permite re-run sin colisión.V3 evita overwrites conceptuales. Cada artefacto relevante tiene versión:
internal_plan_v<N> por documento.execution_plan_v<N> por documento.motor_metadata con rule_versions, llm_versions, input_hash.signal_audit_log versionado.No se sobrescribe nada silenciosamente. Re-decisión produce nuevo versión.
| Dominio | Política | Razón |
|---|---|---|
| LLM call | 3 reintentos, backoff exponencial 1s → 10s | LLM falla por rate limit o glitch transitorio |
| Provider API write | 5-10 reintentos con backoff | Holded a veces 5xx; recovery alto |
| DB read | 10 reintentos rápidos | Casi siempre conexión transitoria |
| DB write con constraint | 1 intento, no retry | Constraint failure = bug; no se arregla solo |
non_retryable=True para input validation errors, schema mismatches contractuales.
Workers arrancan con dos namespaces:
intelia-prod (o intelia-stage): workflows productivos.evals: aislado, para batch evals sin contaminar history productivo.Tests locales saltan Temporal completamente (pipeline invocable directo).
Las actividades son estrechas y propiedad de cada capa:
| Actividad | Capa propietaria |
|---|---|
prepare_facts |
M1 |
decide_account / decide_cost_center / decide_fiscal |
M1 |
compose_internal_plan |
M1 (función pura inline, no activity necesariamente) |
lookup_plan_store |
I1 |
call_plan_generator |
I1 |
execute_provider_group |
I1 |
capture_readback |
I1 |
validate_final |
I1 |
poll_holded_changes |
T4 |
interpret_comments_llm |
T4 |
collect_evidence |
T3 |
shadow_test |
T3 |
promote_rule |
T3 |
signal_projector |
TX |
Las actividades NO llaman servicios cruzados. Cada una pertenece a una capa.
V3 debe responder:
TX es plumería pura. NO contiene:
Si una decisión semántica entra en TX (e.g. "si confidence baja, skip executor"), está en el sitio equivocado. Vive en I1 o S1.
| Riesgo | Mitigación |
|---|---|
| Workflows con lógica de dominio | Lint custom: workflows no llaman a decide_* o interpret_* directamente. Solo orquestan activities |
| Signals se quedan in-memory si workflow crashea | Signal bag persiste en Temporal state. Recoverable on resume |
| Latencia adicional por activities granulares | Activities con failure domain compartido se agrupan |
workflow_id colisión en reprocessing |
version suffix explícito. Check de "ya en progress" antes de start |
| Dedupe de comments rota cuando cambia COMPOSER_VERSION | dedupe_key incluye composer_version |
| LLM activities saturan worker pool | Worker pool tuneado por tipo. LLM activities con pool dedicado |
| Reverse-sync monitor consume recursos forever | continue_as_new periódico para liberar history |
| Temporal history se infla con muchos signals | Signals INTERNAL persisten fuera del workflow (a DB Intelia). Workflow solo carga referencias |
Patología 1: TX se convierte en sitio "por comodidad" para lógica que no encaja. Cuando una decisión no tiene lugar claro, se mete en workflow porque ahí "tiene state". Temporal absorbe decisiones. Mitigación: lint + code review obligatorio.
Patología 2: signal explosion. Cada capa empieza a emitir signals indiscriminadamente. 200+ tipos al año, contable ve 20 tags por documento. Mitigación: catálogo central con review obligatorio. Lint que prohíbe emit_signal("custom_code", ...) con código fuera del registry.
Patología 3: dedupe de comments rota. Comments duplicados en Holded porque dedupe_key incompleto. Mitigación: hash de signal bag completo, no per-signal. Si bag cambia → repost; si no → skip.
Patología 4: Temporal history excesivo. Cada signal añade evento al history. 1000 signals/workflow → history pesa MB. Mitigación: signals INTERNAL fuera del history.
| Métrica | Target |
|---|---|
tx_workflow_latency_p95 |
<60s path feliz, <120s con LLM Plan Generator |
tx_workflow_failure_rate |
<0.5% (excluyendo non-retryable expected) |
tx_signal_projection_success_rate |
>99% |
tx_temporal_history_size_avg |
<500 events por workflow |
tx_reverse_sync_workflow_lag |
<30s desde edit a signal emitido |
tx_dedupe_save_rate |
Mide signals deduplicados — detecta bugs de over-emission |
call_plan_generator es 1 activity o N (una por tool call)? Trade-off granularidad vs latencia.continue_as_new en monitor workflows? Propuesta: cada 1000 events.intelia-prod? Trade-off seguridad y operacional.El target V2 ya tiene transversales Signals + Temporal bien diseñadas. V3 las conserva mayoritariamente, con cambios menores:
El resto del diseño (parent + 2 children, durabilidad, retries por dominio, idempotencia por workflow_id, aislamiento de entornos, pipeline invocable sin Temporal) se mantiene.
Síntesis. Detalle granular en versiones originales.