Los dos planos
Acción efímera y lectura persistente: qué declara cada tool y por qué leer un banco son dos pasos.
Toda tool del registry declara un plane: action o read. El plano es un eje de persistencia de negocio, no de comportamiento. No lo confundas con los behavior hints (readOnly / destructive / idempotent / openWorld), que son otro eje, declarado por separado en cada tool. core.timestamp.now es plane: "action" y a la vez readOnly: true: una tool de acción puede ser de solo lectura, porque lo que la hace «acción» es no persistir nada de negocio; si modifica o no datos es asunto de los hints.
El plano action
Efímero. Nada de negocio se persiste. Dentro del handler, ctx.persist es literalmente null, y escribirle es un error de tipo, no una regla de lint: el acceso a persistencia no existe, por construcción. Lo único que sobrevive a la llamada es la fila de la bitácora, con su digest de auditoría.
Ejemplos de hoy: core.timestamp.now, echo.message.reflect y las tools de indicadores son todas plane: "action". Consultan o calculan algo y responden; no queda una copia normalizada en la base de datos de Connect.
El plano read
Persistente, y reservado a conectores de pago (plan: "paid"). La regla se aplica al construir el registry: plane: "read" exige plan: "paid", y declarar una tool de lectura persistente en un conector gratuito hace fallar el build.
Una tool de plano read sincroniza datos de un sistema externo (el SII, un banco) hacia un modelo normalizado propio, con historial, pensado para que un agente consulte sin golpear la fuente en cada llamada. Ninguno de los conectores gratuitos (core, echo, indicadores) usa el plano read: es el patrón de los conectores con conexión.
Leer son dos pasos, y solo el primero es read
Es la parte contraintuitiva, y conviene tenerla clara antes de la primera llamada contra un banco o el SII. Cada conector con plano read expone exactamente dos clases de tool de datos:
| Qué hace | plane | |
|---|---|---|
conexion.sincronizar | Lo único que contacta al sistema externo y trae datos. Hace login, lee, normaliza y persiste. | read |
<recurso>.consultar | Lee de lo ya persistido. Nunca abre sesión, nunca toca el banco ni el SII. | action |
Sí: la tool que escribe declara read y la que lee declara action, al revés de la intuición. Se entiende al volver al eje: sincronizar alimenta el modelo persistido, por eso declara read; consultar la caché no persiste nada, por eso declara action.
`.consultar` significa siempre «lee la caché»
En todos los conectores; nunca «pregunta en vivo». Si sii.rcv.consultar devuelve una lista vacía, lo que falta es la sincronización de ese período, no los documentos: corre sii.conexion.sincronizar primero y vuelve a consultar. Y al revés: consultar es barato y no gasta un login, así que puedes paginar sin miedo.
No existe una tercera clase. Una tool que consulte en vivo y no persista está prohibida por más natural que parezca: se llevaría un login completo (cerca de 90 segundos en los bancos; en BCI además quema la única sesión activa que el banco permite por usuario) sin dejar nada a cambio, y quien llama no tendría forma de distinguirla de una lectura barata.
Cuál es cuál lo dice el nombre, pero también el catálogo: search_docs({ tool }) en MCP, o GET /v1/tools/{id} en REST, devuelven el plane de cada tool y, en las de consulta, qué alcance leen.
Por qué separar los dos ejes
plane responde «¿persiste esto en Connect?». Los behavior hints responden «¿es segura de reintentar o de auto-ejecutar? ¿muta algo afuera?». Un cliente MCP usa los hints para decidir si puede llamar la tool sin confirmación humana; el pipeline usa plane para decidir si abre un buffer de persistencia. Nunca derives uno del otro.
El modelo, en una llamada
Con la conexión SII de Comercial Aurora SpA ya sincronizada, una consulta responde al instante desde lo persistido:
curl -X POST https://connect.emisso.ai/api/v1/tools/sii.rcv.consultar/execute \
-H "Authorization: Bearer connect_sk_..." \
-H "X-Connect-Connection: conn_9tKfR2mQx4Vb" \
-H "Content-Type: application/json" \
-d '{"input": {"periodo": "2026-07", "perspectiva": "ventas"}}'Respuesta (200, recortada a un documento; la real trae más campos por documento y hasta limit filas por página):
{
"data": {
"documentos": [
{
"tipoDte": 33,
"folio": 4712,
"rutEmisor": "77123456-9",
"rutReceptor": "76543210-3",
"razonSocial": "Constructora Los Robles Ltda",
"fechaEmision": "14/07/2026",
"montoNeto": 1250000,
"montoIva": 237500,
"montoTotal": 1487500,
"estado": "registro",
"periodo": "2026-07",
"perspectiva": "ventas"
}
],
"cursor": null,
"sincronizacion": {
"sincronizadoEn": "2026-08-06T03:15:42.000Z",
"completo": true
}
},
"meta": {
"request_id": "req_...",
"tool_id": "sii.rcv.consultar",
"plane": "action",
"latency_ms": 41,
"audit_status": "recorded"
}
}La llamada no tocó al SII: sincronizacion.sincronizadoEn dice de cuándo son los datos, y la respuesta tardó milisegundos porque solo leyó el plano ya persistido. El recorrido completo (conectar, sincronizar, consultar y los estados intermedios) está en Sincronizar y consultar.