# 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 &#x2A;*`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` [#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](/docs/conceptos/bitacora), 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` [#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](/docs/conceptos/conexiones).

## Leer son dos pasos, y solo el primero es `read` [#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í: &#x2A;*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`.

<Callout type="warn" title="`.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.
</Callout>

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.

<Callout type="info" title="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.
</Callout>

## El modelo, en una llamada [#el-modelo-en-una-llamada]

Con la conexión SII de Comercial Aurora SpA ya sincronizada, una consulta responde al instante desde lo persistido:

```bash
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):

```json
{
  "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](/docs/conceptos/sincronizar-consultar).
