diff --git a/public/docs/runs/runs-01-tab.webp b/public/docs/runs/runs-01-tab.webp new file mode 100644 index 0000000..e2e79a8 Binary files /dev/null and b/public/docs/runs/runs-01-tab.webp differ diff --git a/public/docs/runs/runs-02-new-run.webp b/public/docs/runs/runs-02-new-run.webp new file mode 100644 index 0000000..ef9f7dc Binary files /dev/null and b/public/docs/runs/runs-02-new-run.webp differ diff --git a/public/docs/runs/runs-03-run-page.webp b/public/docs/runs/runs-03-run-page.webp new file mode 100644 index 0000000..a0e53d6 Binary files /dev/null and b/public/docs/runs/runs-03-run-page.webp differ diff --git a/src/content/docs-es/apps.md b/src/content/docs-es/apps.md index 09529d0..5140472 100644 --- a/src/content/docs-es/apps.md +++ b/src/content/docs-es/apps.md @@ -1,7 +1,7 @@ --- title: Apps description: Despliega tus apps desde GitHub a NaN Cloud en minutos. -order: 20 +order: 21 group: Guías --- diff --git a/src/content/docs-es/examples.md b/src/content/docs-es/examples.md index c1cf3ff..d1cac5d 100644 --- a/src/content/docs-es/examples.md +++ b/src/content/docs-es/examples.md @@ -1,7 +1,7 @@ --- title: Ejemplos description: Fragmentos de código para conectarte a la API de NaN con Python, Node.js, curl y más. -order: 19 +order: 20 group: Guías --- diff --git a/src/content/docs-es/intro.md b/src/content/docs-es/intro.md index 1d1fb3a..4d5f140 100644 --- a/src/content/docs-es/intro.md +++ b/src/content/docs-es/intro.md @@ -31,5 +31,6 @@ Los límites van por API key — un tope de peticiones por minuto y un máximo d - [Referencia de la API](/es/docs/api): todos los endpoints, campo a campo. - [Modelos](/es/docs/models): las fichas técnicas y los límites. - [Workspaces](/es/docs/workspaces): tu propia máquina en la nube, por SSH y en el navegador, con agentes de código listos para usar. +- [Runs de agentes](/es/docs/runs): dale una tarea a un agente dentro de tu workspace y deja que trabaje solo. - [Ejemplos](/es/docs/examples): fragmentos en Python, Node.js y curl. - Soporte: reporta incidencias en `#support` de Discord. diff --git a/src/content/docs-es/nan-cli.mdx b/src/content/docs-es/nan-cli.mdx index dd0de9a..57d00c4 100644 --- a/src/content/docs-es/nan-cli.mdx +++ b/src/content/docs-es/nan-cli.mdx @@ -156,6 +156,18 @@ En Linux con bash, el mismo comando pero contra `~/.bashrc`. +## Runs + +Desde la v0.1.25 la CLI también pone a trabajar agentes en tareas dentro de tus [workspaces](/es/docs/workspaces), y los sigue desde tu terminal: + +```bash +nan run "añade tests al parser de fechas y haz que pasen" +nan runs ls +nan runs logs -f +``` + +El run se ejecuta en el workspace, no en tu máquina, así que sigue aunque cierres la terminal. Los comandos de runs usan tu sesión de `nan auth login`, o un token de plataforma desde `NAN_TOKEN`, `--token-file` o `nan auth login --api-token` para scripts, CI y servidores. Todos los flags, el orden de autenticación, los códigos de salida y la API están en [Runs de agentes](/es/docs/runs). + ## Actualizar Vuelve a pasar el instalador. Siempre trae la última versión publicada y sobrescribe la que tengas: diff --git a/src/content/docs-es/runs.mdx b/src/content/docs-es/runs.mdx new file mode 100644 index 0000000..75ed923 --- /dev/null +++ b/src/content/docs-es/runs.mdx @@ -0,0 +1,436 @@ +--- +title: Runs de agentes +description: Dale una tarea a un agente dentro de tu workspace y deja que trabaje solo, desde el portal, la terminal o la API. +order: 19 +group: Guías +--- + +import Steps from '../../components/docs/Steps.astro'; +import Details from '../../components/docs/Details.astro'; +import Callout from '../../components/docs/Callout.astro'; +import Screenshot from '../../components/docs/Screenshot.astro'; + +# Runs de agentes. + +Un **run** le da una tarea a un agente dentro de uno de tus [workspaces](/es/docs/workspaces) y lo deja trabajar solo. Escribes lo que quieres que haga, eliges el agente (Pi o Hermes, el que tengas instalado en ese workspace) y la carpeta, y el run se encarga del resto. Sigue adelante aunque cierres la página o la terminal. + +Mientras trabaja, sigues su log en directo. Cuando termina tienes su resultado y, si la carpeta es un repositorio git, una rama con sus cambios lista para que la revises. + +Puedes lanzar un run de tres formas, y las tres ven los mismos runs: + +- Desde la pestaña **Runs** de tu workspace en el portal. +- Desde tu terminal, con `nan run`. +- Desde tus propios scripts, con la API. + +## Antes de empezar + +- **Un workspace encendido**, con Pi o Hermes instalado. Puedes instalar cualquiera de los dos desde el panel de **Software** del workspace. +- **Un plan con inferencia.** Un run usa la clave de inferencia del propio workspace, y lo que gasta el agente cuenta para el uso de tu plan como cualquier otra petición. Si tu plan no incluye inferencia no puedes crear runs: el portal te enseña las opciones para mejorarlo. + +## Lanza un run desde el portal + + + +### Abre la pestaña Runs + +Entra en [cloud.nan.builders/workspaces](https://cloud.nan.builders/workspaces) y abre un workspace. Junto a **Overview**, **Console**, **SSH keys**, **Events** y **Backups** tienes la pestaña **Runs**. Su tarjeta **Agent runs** lista los runs de ese workspace, del más reciente al más antiguo, cada uno con su estado, el agente, la tarea, cuándo empezó, cuánto duró y los tokens que gastó. + + + +### Rellena el formulario + +Pulsa **New run** y rellena el formulario (el portal está en inglés): + +| Campo | Qué es | +|---|---| +| **Agent** | El agente: Pi o Hermes, el que tengas instalado en el workspace. | +| **Task** | La tarea, con tus palabras, como se la escribirías al agente en un chat. Hasta 32 KiB (el contador bajo la caja te dice los bytes que llevas). | +| **Folder** | La carpeta donde trabaja el agente. Por defecto es `/home/nan` y tiene que estar dentro de ella. | +| **Separate branch** | Rama aparte: **Auto** (por defecto), **On** u **Off**. En un repositorio git, el agente trabaja en su propia rama y tu checkout se queda como está. Consulta [Dónde trabaja el agente](#dónde-trabaja-el-agente). | +| **Time limit** | El tiempo máximo: **30 min** por defecto, igual que en la CLI y la API. Las opciones son 15 min, 30 min, 1 hour y 2 hours (1 hora y 2 horas). El run se para al llegar a él. | + +Pulsa **Start run**, o `Ctrl+Enter` (`⌘+Enter` en un Mac) desde la caja de la tarea. + + + +### Síguelo en directo + +Haz clic en un run de la lista para abrir su página. Arriba ves su estado, el workspace, el agente, la carpeta, cuánto lleva, los tokens que ha gastado, su tiempo máximo y la tarea. Debajo, en **Output**, el log se va llenando en directo mientras el agente trabaja: lo que dice, las herramientas que usa y lo que le responden. Haz clic en una línea **Tool call** o **Tool result** para desplegarla. Cuando el run termina, la página muestra cómo ha acabado, el resumen del agente y, si la hay, la rama con sus cambios. + +Puedes cerrar la página cuando quieras. El run sigue, y su página estará ahí cuando vuelvas. + + + +### Cancélalo, si hace falta + +**Cancel run** para el run en unos segundos. El portal te pide confirmación antes. + + + +## Dónde trabaja el agente + +Lo que pasa con tus archivos depende de la carpeta (**Folder**) que elijas y de la opción **Separate branch**. + +| La carpeta... | **Auto** (por defecto) | **On** | **Off** | +|---|---|---|---| +| está dentro de un repositorio git | Rama aparte | Rama aparte | Directamente en la carpeta | +| no es un repositorio git | Directamente en la carpeta | El run falla con un error de configuración | Directamente en la carpeta | + +**Con rama aparte**, el agente no toca tu checkout. Trabaja en una copia aparte del repositorio (un git worktree), en una rama nueva que se llama `nan-run/` seguido de los 8 primeros caracteres del id del run, por ejemplo `nan-run/6f1c2a9b`. Cuando el agente acaba, todo lo que ha cambiado queda en un commit en esa rama. El run **nunca hace push**: la rama se queda en el repositorio dentro de tu workspace y tú decides qué hacer con ella. Si el agente no ha cambiado nada, la rama se borra. + +Para revisar los cambios e incorporarlos, abre una terminal en tu workspace ([por SSH o desde la pestaña Console](/es/docs/workspaces#conectarte)) y, desde el repositorio: + +```bash +git log -p HEAD..nan-run/6f1c2a9b +git merge nan-run/6f1c2a9b +``` + +**Sin rama aparte**, el agente trabaja directamente en la carpeta y no hay rama. Úsalo con carpetas que no son repositorios, o cuando quieras los cambios ahí mismo. + +La carpeta tiene que estar dentro de `/home/nan`, tu carpeta personal en el workspace. + +## Estados de un run + +| Estado | Qué significa | +|---|---| +| `queued` | Esperando un hueco libre. Consulta [Cuántos runs a la vez](#cuántos-runs-a-la-vez). | +| `starting` | Se está preparando en el workspace. | +| `running` | El agente está trabajando. | +| `succeeded` | El agente ha terminado su tarea. | +| `failed` | El agente se ha parado con un error, o el run no ha podido empezar (por ejemplo, **Separate branch** en **On** en una carpeta que no es un repositorio). | +| `cancelled` | Lo has cancelado tú. | +| `timed_out` | Ha llegado a su tiempo máximo y se ha parado. | + +El portal muestra los mismos estados en inglés y con mayúscula, por ejemplo **Running**, **Succeeded** o **Cancelled**. + +Desde la CLI y la API el tiempo máximo va de 60 segundos a 2 horas. El portal ofrece 15 min, 30 min, 1 hora y 2 horas, con 30 min por defecto. + +## Cuántos runs a la vez + +Cuántos runs trabajan a la vez depende del tamaño del workspace: + +| Tamaño | Runs a la vez | +|---|---| +| Micro (también el workspace gratis de Premium) | 1 | +| Nano | 2 | +| Basic | 3 | +| Medium | 5 | +| Large | 8 | + +Además, los runs comparten tu concurrencia de inferencia, así que puedes tener como mucho **5 runs trabajando a la vez entre todos tus workspaces** (**8 con Premium**). + +Los runs que pasan de esos límites no se rechazan: esperan en una cola y arrancan por orden de llegada. En la cola caben hasta **20 runs por workspace** y **50 en total**, y puedes crear hasta **60 runs por hora**. + +Cada run trabaja en su propio proceso dentro del workspace, con su parte de CPU y de memoria. Si uno se descontrola, se para solo ese: tus sesiones SSH y tus agentes siempre encendidos siguen funcionando. + +## Inferencia y secretos + +Los runs usan la clave de inferencia del workspace, la misma que usan sus agentes cuando trabajas con ellos a mano. Sus peticiones cuentan para el uso de tu plan como cualquier otra. + +Los secretos del archivo de entorno de tu workspace, como esa clave de inferencia o un token de bot de Telegram, aparecen como `***` en los logs y resultados de los runs, así que un log no los filtra. + +## Desde la terminal: `nan run` + +La [CLI de NaN](/es/docs/nan-cli) lanza y sigue runs desde tu propia terminal. Necesitas la versión **0.1.25** o posterior: `nan --version` imprime `nan `. + + + +### Instala o actualiza la CLI + +```bash +curl -fsSL https://nan.builders/install | bash +``` + +### Inicia sesión + +```bash +nan auth login --email tu@correo +``` + +Te llega por correo un enlace de acceso. Cópialo y pégalo de vuelta en el comando sin abrirlo antes en el navegador: solo funciona una vez. Tienes los detalles en la [guía de la CLI](/es/docs/nan-cli). Para scripts, CI o un servidor, usa mejor un token: consulta [Autenticación](#autenticación). + +### Lanza un run + +```bash +nan run "añade tests al parser de fechas y haz que pasen" +``` + +La CLI te muestra el log en la terminal hasta que el run termina. + + + +Otras formas de lanzarlo: + +```bash +# Un workspace, un agente y una carpeta concretos +nan run --ws develop --agent hermes --cwd /home/nan/projects/api "revisa los TODO pendientes" + +# La tarea desde un archivo, o desde stdin +nan run -f tarea.md +git diff | nan run - + +# Lánzalo y quédate con el id sin esperar +nan run --detach "actualiza las dependencias" +``` + +| Flag | Por defecto | Qué hace | +|---|---|---| +| `--ws NAME` | tu único workspace | El workspace donde se ejecuta. Si tienes más de uno y no lo pones, la CLI te los lista y sale con código 64. | +| `--agent pi\|hermes` | `pi` | El agente. | +| `--model M` | el del agente | El modelo que usa el agente. | +| `--cwd PATH` | `/home/nan` | La carpeta en el workspace. | +| `--worktree` / `--no-worktree` | auto | Fuerza la rama aparte a on u off. Consulta [Dónde trabaja el agente](#dónde-trabaja-el-agente). | +| `--timeout D` | `30m` | El tiempo máximo, de `1m` a `2h`. | +| `--detach` | | Lanza el run, imprime su id y vuelve. | +| `--idempotency-key K` | | Si un reintento manda la misma clave y la misma petición, recibes el run ya creado en lugar de uno nuevo. | +| `--json` | | Imprime los eventos como líneas JSON y después el run terminado. | +| `-f FILE` | | Lee la tarea de un archivo, o de stdin con `-`. | +| `--token-file PATH` | | Se autentica con el token de la primera línea de un archivo. Consulta [Autenticación](#autenticación). | + +Los flags equivalen al formulario del portal: **Agent** es `--agent`, **Folder** es `--cwd`, **Separate branch** es `--worktree` / `--no-worktree` y **Time limit** es `--timeout`. La tarea es el texto entre comillas. + +Mientras se muestra el log, **Ctrl-C una vez** te desconecta del log: el run sigue y la CLI te dice cómo retomarlo. **Ctrl-C dos veces en menos de 2 segundos** lo cancela. + +### Gestiona tus runs + +```bash +nan runs ls # tus runs, del más reciente al más antiguo +nan runs show # estado, resultado y prompt +nan runs logs -f # sigue el log hasta que termine +nan runs cancel +``` + +`nan runs logs --json` imprime un evento JSON por línea, cómodo para scripts. Cada evento tiene un `type`; estos son los principales: + +| Tipo | Qué es | +|---|---| +| `started` | El run ha empezado: agente, modelo, carpeta y, si la hay, la rama. | +| `message` | Texto del agente. | +| `tool_call` / `tool_result` | Una herramienta que ha usado el agente, y lo que le ha respondido. | +| `log` | Una línea de salida que no es un mensaje. | +| `usage` | Los tokens gastados hasta ahora. | +| `error` | Algo ha ido mal, con un código. | +| `finished` | El run ha terminado: su estado y el resumen del agente. | + +### Códigos de salida + +`nan run` y `nan runs logs -f` salen con el resultado del run, así que un script puede actuar sin leer el texto: + +| Código | Significado | +|---|---| +| 0 | terminado con éxito | +| 1 | fallido | +| 2 | se agotó el tiempo máximo | +| 3 | cancelado | +| 4 | el workspace no está listo: agente no instalado, sin clave de inferencia o configuración incorrecta | +| 64 | error de uso, también tener más de un workspace y no pasar `--ws` | +| 65 | sin sesión, sesión caducada o sin permiso | +| 69 | nan.builders no disponible, o se ha perdido el stream (el run sigue, y se imprime el id) | +| 75 | el run se ha aceptado y sigue en marcha (`--detach`, o Ctrl-C una vez) | + +Con `set -e`, ten en cuenta el 75 de `--detach`: + +```bash +id=$(nan run --detach "actualiza las dependencias") || [ $? -eq 75 ] +``` + +### Autenticación + +`nan run` y `nan runs` usan lo primero que encuentren de esta lista: + +1. **`NAN_TOKEN`**: una variable de entorno con un token de plataforma (`nan_pat_...`) o una API key (`sk-...`). Se lee en cada comando y nunca se escribe en disco. +2. **`--token-file PATH`**: el token en la primera línea de un archivo. El archivo tiene que ser privado (`chmod 600`): en macOS y Linux, si otros usuarios pueden leerlo o escribirlo, se rechaza. +3. **Un token guardado**, con `nan auth login --api-token`. +4. **Tu sesión**, de `nan auth login` (el enlace de acceso por correo). +5. **Una API key guardada**, desde la pestaña Setup (o una clave `sk-...` guardada con `nan auth login --api-token`). + +`nan auth login --api-token` lee el token solo de stdin, oculto mientras lo pegas, lo comprueba contra nan.builders y lo guarda con permisos `0600`. Lo guarda aparte de la API key, así que la pestaña Setup nunca copia un token de plataforma en la configuración de tus herramientas: + +```bash +nan auth login --api-token # pégalo cuando te lo pida +nan auth login --api-token < token.txt # o pásalo por stdin +``` + +No pongas nunca un token en la propia línea de comandos: acabaría en el historial de tu shell y en `ps`. + +En la CLI, un token de plataforma solo sirve para `nan run` y `nan runs` (consulta [API keys y tokens](#api-keys-y-tokens)). `nan me`, `nan metrics usage` y el panel siguen necesitando una sesión de `nan auth login`. + +### Usa la CLI en CI o en un servidor + +Crea un [token](#api-keys-y-tokens), guárdalo en los secretos de tu CI y pásalo como `NAN_TOKEN`. Por ejemplo, en GitHub Actions: + +```yaml +- name: Review the PR with an agent + env: + NAN_TOKEN: ${{ secrets.NAN_TOKEN }} + run: | + curl -fsSL https://nan.builders/install | bash + nan run --ws ci --timeout 20m -f .github/review-task.md +``` + +Ese paso espera al run y falla si el run falla. Para lanzarlo y seguir sin esperar, usa `--detach` y ten en cuenta su código de salida 75: + +```bash +nan run --ws ci --detach "actualiza las dependencias" || [ $? -eq 75 ] +``` + + +Guarda el token solo como secreto (`secrets.NAN_TOKEN` en GitHub Actions), nunca en el repositorio ni en el archivo del workflow. Puede lanzar runs que gastan tu plan. + + +En un servidor que configuras una vez, guárdalo mejor con `nan auth login --api-token`. + +## Desde la API + +Los mismos runs están disponibles por HTTP, en `https://api.nan.builders/v1/runs`, para tus scripts e integraciones. + +### API keys y tokens + +Hay dos tipos de credenciales que sirven para la API de runs. Las dos están en tus [ajustes del portal](https://cloud.nan.builders/): + +| | **API Keys** | **Tokens** | +|---|---|---| +| Formato | `sk-...` | `nan_pat_...` | +| Inferencia (modelos) | Sí | **No, nunca** | +| API de la plataforma (runs, tus workspaces) | Sí | Sí | +| Quién puede crearlas | Planes con inferencia | Cualquier miembro con una suscripción activa | + +Un **token** es lo que conviene darle a un script que solo gestiona runs. No sirve para llamar a los modelos directamente, pero sí para lanzar runs que gastan tu plan: guárdalo igual de bien que una API key y revócalo si se filtra. Créalo en **Settings → Tokens**: + +- Se muestra **una sola vez**, al crearlo. Cópialo en ese momento. +- Puedes tener hasta **10** tokens activos. +- Cada uno caduca a los **30, 90 o 365 días**, o nunca. +- Puedes revocar cualquiera en cualquier momento. + +Para crear runs sigue haciendo falta un plan con inferencia, uses la credencial que uses. + + +La clave que hay dentro de un workspace, la que usan sus agentes para la inferencia, no puede gestionar runs: la API le responde con un `403`. Un agente que trabaja en tu workspace no puede lanzar runs nuevos por su cuenta, salvo que le des una de tus propias API keys o tokens. + + +### Crea un run + +```bash +export NAN_TOKEN="nan_pat_..." + +curl https://api.nan.builders/v1/runs \ + -H "Authorization: Bearer $NAN_TOKEN" \ + -H "Content-Type: application/json" \ + -H "Idempotency-Key: fix-date-parser-1" \ + -d '{ + "workspace": "develop", + "agent": "pi", + "prompt": "añade tests al parser de fechas y haz que pasen", + "cwd": "/home/nan/projects/api", + "git_isolation": null, + "timeout_seconds": 1800 + }' +``` + +| Campo | Obligatorio | Qué es | +|---|---|---| +| `workspace` | Sí | Nombre o id del workspace. | +| `agent` | Sí | `pi` o `hermes`. | +| `prompt` | Sí | La tarea, hasta 32 KiB. | +| `cwd` | No | La carpeta, `/home/nan` por defecto. | +| `git_isolation` | No | Rama aparte: `null` para auto (por defecto), `true` para on, `false` para off. | +| `timeout_seconds` | No | El tiempo máximo, de 60 a 7200. 1800 (30 minutos) por defecto. | +| `model` | No | El modelo del agente. Por defecto, el suyo. | + +Los campos equivalen al formulario del portal: **Agent** es `agent`, **Task** es `prompt`, **Folder** es `cwd`, **Separate branch** es `git_isolation` y **Time limit** es `timeout_seconds`. + +La respuesta es el run, con su `id` y su `state`. La cabecera `Idempotency-Key` es opcional: si un reintento manda la misma clave con el mismo cuerpo, recibes el run que ya se creó en lugar de uno nuevo. + +### Síguelo + +Los eventos de un run llegan como un stream de server-sent events cuando pides `text/event-stream`. Con `curl -N` los ves según llegan: + +```bash +curl -N https://api.nan.builders/v1/runs/$RUN_ID/events \ + -H "Authorization: Bearer $NAN_TOKEN" \ + -H "Accept: text/event-stream" +``` + +Cada evento del stream es uno de estos: + +- `event: run_event`, con una línea `id:`: un evento del log (un mensaje, una llamada a una herramienta, su resultado...). Manda el último `id` que hayas visto en la cabecera `Last-Event-ID` para retomar tras un corte, sin perder ni repetir nada. +- `event: state`: el run ha cambiado de estado. +- `event: end`: el run ha terminado y su log se ha enviado entero. Los datos son el run terminado. El stream se cierra después. + +Cada 15 segundos el stream manda una línea de comentario para mantener la conexión abierta. Un stream dura como mucho 30 minutos: si se cierra antes del `end`, vuelve a conectarte con `Last-Event-ID` y sigue donde lo dejaste. Sin la cabecera `Accept`, el mismo endpoint responde con páginas de JSON: pasa el `next_after` de cada página como `?after=` para pedir la siguiente. + +Para ver cómo ha terminado: + +```bash +curl https://api.nan.builders/v1/runs/$RUN_ID \ + -H "Authorization: Bearer $NAN_TOKEN" +``` + +### Cancélalo + +```bash +curl -X POST https://api.nan.builders/v1/runs/$RUN_ID/cancel \ + -H "Authorization: Bearer $NAN_TOKEN" +``` + +### Lista tus runs + +```bash +curl "https://api.nan.builders/v1/runs?workspace=develop&state=running&limit=20" \ + -H "Authorization: Bearer $NAN_TOKEN" +``` + +Todos los filtros son opcionales. Si hay más runs de los que caben en una página, la respuesta trae un `next_cursor`: pásalo como `?cursor=` para pedir la siguiente. + +### Errores + +Los errores usan el mismo formato que la API de inferencia: + +```json +{"error": {"message": "...", "type": "...", "param": null, "code": "tier_restricted"}} +``` + +| Estado y código | Qué hacer | +|---|---| +| `400 invalid_request` | Hay un campo incorrecto. `param` te dice cuál. | +| `403 tier_restricted` | Tu plan no incluye inferencia. Mejóralo para crear runs. | +| `403 workspace_key_not_allowed` | Has usado la clave de dentro de un workspace. Usa una API key o un token. | +| `404` | El workspace o el run no existe, o no es tuyo. | +| `409 idempotency_conflict` | Has repetido una `Idempotency-Key` con otro cuerpo. Usa una clave nueva. | +| `409 workspace_not_running` | Enciende primero el workspace. | +| `409 agent_not_installed` | Instala el agente desde el panel de **Software**, o elige el otro. | +| `409 no_inference_key` | El workspace no tiene clave de inferencia. Comprueba que tu plan incluye inferencia. | +| `429 run_queue_full` | La cola está llena (20 por workspace, 50 en total). Espera a que terminen algunos runs. | +| `429 rate_limited` | Más de 60 runs en la última hora. Vuelve a intentarlo más tarde. | + +## Preguntas frecuentes + +
+ +No. Con rama aparte, los cambios se quedan en una rama `nan-run/...` del repositorio dentro de tu workspace. Hacer push, merge o descartarlos es cosa tuya. + +
+ +
+ +Nada: el run sigue en el workspace. Abre su página en la pestaña **Runs**, o ejecuta `nan runs logs -f`, para retomarlo. + +
+ +
+ +No. Los runs usan los modelos a través de la clave de inferencia del workspace, así que para crearlos necesitas un plan con inferencia. Sí puedes crear un token y listar tus workspaces y tus runs. + +
+ +
+ +No. Cada run tiene su propia parte de la CPU y la memoria del workspace. Si uno se descontrola, solo se para ese run. + +
+ +
+ +Escribe en `#support` en Discord. + +
diff --git a/src/content/docs-es/workspaces.mdx b/src/content/docs-es/workspaces.mdx index b8ca31d..3a9494e 100644 --- a/src/content/docs-es/workspaces.mdx +++ b/src/content/docs-es/workspaces.mdx @@ -215,6 +215,12 @@ gentle-shell es un complemento de Pi (memoria entre sesiones con Engram), así q Con un plan de inferencia, los agentes vienen configurados para usar los modelos de NaN con tu membresía. Las guías de [Pi](/es/docs/pi) y [Hermes](/es/docs/hermes) explican cómo cambiar el modelo o el proveedor a mano. +## Pon a tus agentes a trabajar en tareas + +No hace falta que te quedes conectado mientras un agente trabaja. Un **run** le da una tarea a Pi o a Hermes dentro de tu workspace y lo deja trabajar solo: sigues su log en directo, continúa aunque cierres la página y, en un repositorio git, sus cambios acaban en una rama para que los revises. Lánzalo desde la pestaña **Runs** del workspace, desde tu terminal con `nan run` o desde la API. + +Cómo funciona, los límites y los comandos están en [Runs de agentes](/es/docs/runs). + ## Copias de seguridad Los workspaces **no tienen copias de seguridad** salvo que actives el **complemento de backups** de ese workspace, desde su pestaña **Backups**. diff --git a/src/content/docs/apps.md b/src/content/docs/apps.md index 1dd74bf..f5f4d36 100644 --- a/src/content/docs/apps.md +++ b/src/content/docs/apps.md @@ -1,7 +1,7 @@ --- title: Apps description: Deploy your apps from GitHub to NaN Cloud in minutes. -order: 20 +order: 21 group: Guides --- diff --git a/src/content/docs/examples.md b/src/content/docs/examples.md index 3a4ab7d..1a59d7b 100644 --- a/src/content/docs/examples.md +++ b/src/content/docs/examples.md @@ -1,7 +1,7 @@ --- title: Examples description: Code snippets to connect to the NaN API with Python, Node.js, curl, and more. -order: 19 +order: 20 group: Guides --- diff --git a/src/content/docs/intro.md b/src/content/docs/intro.md index 979ead2..14f24db 100644 --- a/src/content/docs/intro.md +++ b/src/content/docs/intro.md @@ -31,5 +31,6 @@ Limits are per API key — a cap on requests per minute and a maximum number of - [API reference](/docs/api): every endpoint, field by field. - [Models](/docs/models): the spec sheets and the limits. - [Workspaces](/docs/workspaces): your own cloud machine, over SSH and in the browser, with coding agents ready to use. +- [Agent runs](/docs/runs): give an agent a task inside your workspace and let it work on its own. - [Examples](/docs/examples): snippets in Python, Node.js and curl. - Support: report issues in `#support` on Discord. diff --git a/src/content/docs/nan-cli.mdx b/src/content/docs/nan-cli.mdx index 0d62b44..9fb1916 100644 --- a/src/content/docs/nan-cli.mdx +++ b/src/content/docs/nan-cli.mdx @@ -156,6 +156,18 @@ On Linux with bash, the same command but against `~/.bashrc`. +## Runs + +From v0.1.25 the CLI also starts agents on tasks inside your [workspaces](/docs/workspaces), and follows them from your terminal: + +```bash +nan run "add tests for the date parser and make them pass" +nan runs ls +nan runs logs -f +``` + +The run happens in the workspace, not on your machine, so it keeps going if you close the terminal. The runs commands use your `nan auth login` session, or a platform token from `NAN_TOKEN`, `--token-file` or `nan auth login --api-token` for scripts, CI and servers. Every flag, the authentication order, the exit codes and the API are in [Agent runs](/docs/runs). + ## Updating Run the installer again. It always brings the latest published release and overwrites the one you have: diff --git a/src/content/docs/runs.mdx b/src/content/docs/runs.mdx new file mode 100644 index 0000000..1fedabe --- /dev/null +++ b/src/content/docs/runs.mdx @@ -0,0 +1,436 @@ +--- +title: Agent runs +description: Give an agent a task inside your workspace and let it work on its own, from the portal, the terminal or the API. +order: 19 +group: Guides +--- + +import Steps from '../../components/docs/Steps.astro'; +import Details from '../../components/docs/Details.astro'; +import Callout from '../../components/docs/Callout.astro'; +import Screenshot from '../../components/docs/Screenshot.astro'; + +# Agent runs. + +A **run** gives an agent a task inside one of your [workspaces](/docs/workspaces) and lets it work on its own. You write what you want done, pick the agent (Pi or Hermes, whichever is installed in that workspace) and the folder, and the run takes it from there. It keeps going if you close the page or the terminal. + +While it works you follow its log live. When it ends you get its result and, if the folder is a git repository, a branch with its changes, ready for you to review. + +You can start a run in three ways, and all three see the same runs: + +- From the **Runs** tab of your workspace in the portal. +- From your terminal, with `nan run`. +- From your own scripts, with the API. + +## Before you start + +- **A workspace that is running**, with Pi or Hermes installed. You can install either one from the **Software** panel of the workspace. +- **A plan with inference.** A run uses your workspace's own inference key, and what the agent spends counts against your plan's usage like any other request. Without inference in your plan you cannot create runs: the portal shows you the upgrade options instead. + +## Start a run from the portal + + + +### Open the Runs tab + +Go to [cloud.nan.builders/workspaces](https://cloud.nan.builders/workspaces) and open a workspace. Next to **Overview**, **Console**, **SSH keys**, **Events** and **Backups** there is a **Runs** tab. Its **Agent runs** card lists the runs of that workspace, newest first, each with its state, the agent, the task, when it started, how long it took and the tokens it used. + + + +### Fill in the form + +Press **New run** and fill in the form: + +| Field | What it is | +|---|---| +| **Agent** | Pi or Hermes, whichever is installed in the workspace. | +| **Task** | What you want done, in your own words, as you would write it to the agent in a chat. Up to 32 KiB (the counter under the box shows the bytes used). | +| **Folder** | Where the agent works. It defaults to `/home/nan` and must be inside it. | +| **Separate branch** | **Auto** (the default), **On** or **Off**. In a git repository, the agent works on its own branch and your checkout stays as it is. See [Where the agent works](#where-the-agent-works). | +| **Time limit** | **30 min** by default, the same as the CLI and the API. The options are 15 min, 30 min, 1 hour and 2 hours. The run is stopped when it reaches it. | + +Press **Start run**, or `Ctrl+Enter` (`⌘+Enter` on a Mac) from the task box. + + + +### Follow it live + +Click a run in the list to open its page. At the top you see its state, the workspace, the agent, the folder, how long it has been running, the tokens it has used, its time limit and the task. Below, under **Output**, the log fills in live as the agent works: what it says, the tools it calls and what they answer. Click a **Tool call** or **Tool result** line to unfold it. When the run ends, the page shows how it ended, the agent's summary and, if there is one, the branch with its changes. + +You can close the page at any moment. The run goes on, and its page is there when you come back. + + + +### Cancel it, if you need to + +**Cancel run** stops the run within a few seconds. The portal asks you to confirm first. + + + +## Where the agent works + +What happens to your files depends on the **Folder** you choose and on the **Separate branch** setting. + +| The folder is... | **Auto** (default) | **On** | **Off** | +|---|---|---|---| +| inside a git repository | Separate branch | Separate branch | Directly in the folder | +| not a git repository | Directly in the folder | The run fails with a configuration error | Directly in the folder | + +**With a separate branch**, the agent does not touch your checkout. It works in a separate copy of the repository (a git worktree), on a new branch called `nan-run/` followed by the first 8 characters of the run id, for example `nan-run/6f1c2a9b`. When the agent ends, whatever it changed is committed on that branch. The run **never pushes**: the branch stays in the repository inside your workspace, and you decide what to do with it. If the agent changed nothing, the branch is deleted. + +To review the changes and bring them in, open a terminal in your workspace ([over SSH or the Console tab](/docs/workspaces#connect)) and, from the repository: + +```bash +git log -p HEAD..nan-run/6f1c2a9b +git merge nan-run/6f1c2a9b +``` + +**Without a separate branch**, the agent works directly in the folder and there is no branch. Use it for folders that are not repositories, or when you want the changes right there. + +The folder must be inside `/home/nan`, your home in the workspace. + +## Run states + +| State | What it means | +|---|---| +| `queued` | Waiting for a free place. See [How many runs at once](#how-many-runs-at-once). | +| `starting` | Being set up in the workspace. | +| `running` | The agent is working. | +| `succeeded` | The agent finished its task. | +| `failed` | The agent stopped with an error, or the run could not start (for example, **Separate branch** set to **On** in a folder that is not a repository). | +| `cancelled` | You cancelled it. | +| `timed_out` | It reached its time limit and was stopped. | + +The portal shows the same states with a capital letter, for example **Running**, **Succeeded** or **Cancelled**. + +Through the CLI and the API the time limit goes from 60 seconds to 2 hours. The portal offers 15 min, 30 min, 1 hour and 2 hours, with 30 min by default. + +## How many runs at once + +How many runs work at the same time depends on the size of the workspace: + +| Size | Runs at once | +|---|---| +| Micro (also the free Premium workspace) | 1 | +| Nano | 2 | +| Basic | 3 | +| Medium | 5 | +| Large | 8 | + +On top of that, runs share your inference concurrency, so you can have at most **5 runs working at once across all your workspaces** (**8 on Premium**). + +Runs beyond those limits are not refused: they wait in a queue and start in order, first in, first out. The queue holds up to **20 runs per workspace** and **50 in total**, and you can create up to **60 runs per hour**. + +Each run works in its own process inside the workspace, with its own share of CPU and memory. If one gets out of hand, it is stopped alone: your SSH sessions and your always-on agents keep working. + +## Inference and secrets + +Runs use the inference key of the workspace, the same one its agents use when you work with them by hand. Their requests count against your plan's usage like any other. + +Secrets in your workspace's environment file, such as that inference key or a Telegram bot token, are shown as `***` in run logs and results, so a log does not leak them. + +## From the terminal: `nan run` + +The [NaN CLI](/docs/nan-cli) starts and follows runs from your own terminal. You need version **0.1.25** or newer: `nan --version` prints `nan `. + + + +### Install or update the CLI + +```bash +curl -fsSL https://nan.builders/install | bash +``` + +### Sign in + +```bash +nan auth login --email you@yours +``` + +It emails you a sign-in link. Copy it from the email and paste it back into the command without opening it in the browser first: it works once. The [NaN CLI guide](/docs/nan-cli) has the details. For scripts, CI or a server, use a token instead: see [Authentication](#authentication). + +### Start a run + +```bash +nan run "add tests for the date parser and make them pass" +``` + +The CLI streams the log to your terminal until the run ends. + + + +A few more ways to start one: + +```bash +# A specific workspace, agent and folder +nan run --ws develop --agent hermes --cwd /home/nan/projects/api "review the open TODOs" + +# The task from a file, or from stdin +nan run -f task.md +git diff | nan run - + +# Start it and get the id back without waiting +nan run --detach "upgrade the dependencies" +``` + +| Flag | Default | What it does | +|---|---|---| +| `--ws NAME` | your only workspace | The workspace to run in. If you have more than one and leave it out, the CLI lists them and exits with code 64. | +| `--agent pi\|hermes` | `pi` | The agent. | +| `--model M` | the agent's own | The model the agent uses. | +| `--cwd PATH` | `/home/nan` | The folder in the workspace. | +| `--worktree` / `--no-worktree` | auto | Force a separate branch on or off. See [Where the agent works](#where-the-agent-works). | +| `--timeout D` | `30m` | The time limit, from `1m` to `2h`. | +| `--detach` | | Start the run, print its id and return. | +| `--idempotency-key K` | | If a retry sends the same key and request, you get the run already created instead of a second one. | +| `--json` | | Print events as JSON lines, then the finished run. | +| `-f FILE` | | Read the task from a file, or from stdin with `-`. | +| `--token-file PATH` | | Authenticate with the token on the first line of a file. See [Authentication](#authentication). | + +The flags match the portal form: **Agent** is `--agent`, **Folder** is `--cwd`, **Separate branch** is `--worktree` / `--no-worktree` and **Time limit** is `--timeout`. The task is the text in quotes. + +While it streams, **Ctrl-C once** stops following the log: the run keeps going and the CLI tells you how to pick it up again. **Ctrl-C twice within 2 seconds** cancels it. + +### Manage your runs + +```bash +nan runs ls # your runs, newest first +nan runs show # state, result and prompt +nan runs logs -f # follow the log until it ends +nan runs cancel +``` + +`nan runs logs --json` prints one JSON event per line, handy for scripts. Each event has a `type`; these are the main ones: + +| Type | What it is | +|---|---| +| `started` | The run started: agent, model, folder and, if there is one, the branch. | +| `message` | Text from the agent. | +| `tool_call` / `tool_result` | A tool the agent called, and what it answered. | +| `log` | A line of output that is not a message. | +| `usage` | Tokens used so far. | +| `error` | Something went wrong, with a code. | +| `finished` | The run ended: its state and the agent's summary. | + +### Exit codes + +`nan run` and `nan runs logs -f` exit with the outcome of the run, so a script can act on it without reading the text: + +| Code | Meaning | +|---|---| +| 0 | succeeded | +| 1 | failed | +| 2 | timed out | +| 3 | cancelled | +| 4 | the workspace is not ready for it: agent not installed, no inference key or a bad configuration | +| 64 | usage error, including more than one workspace and no `--ws` | +| 65 | not signed in, session expired or not allowed | +| 69 | nan.builders unavailable, or the stream was lost (the run goes on, and the id is printed) | +| 75 | the run was accepted and is still going (`--detach`, or Ctrl-C once) | + +Under `set -e`, allow for the 75 of `--detach`: + +```bash +id=$(nan run --detach "upgrade the dependencies") || [ $? -eq 75 ] +``` + +### Authentication + +`nan run` and `nan runs` use the first of these they find: + +1. **`NAN_TOKEN`**: an environment variable holding a platform token (`nan_pat_...`) or an API key (`sk-...`). It is read on every command and never written to disk. +2. **`--token-file PATH`**: the token on the first line of a file. The file must be private (`chmod 600`): on macOS and Linux, one that other users can read or write is refused. +3. **A saved token**, from `nan auth login --api-token`. +4. **Your session**, from `nan auth login` (the emailed sign-in link). +5. **A saved API key**, from the Setup tab (or an `sk-...` key saved with `nan auth login --api-token`). + +`nan auth login --api-token` reads the token from stdin only, hidden while you paste it, checks it against nan.builders and saves it with mode `0600`. It keeps it apart from the API key, so the Setup tab never copies a platform token into your tools' configuration: + +```bash +nan auth login --api-token # paste it when asked +nan auth login --api-token < token.txt # or pipe it in +``` + +Never put a token on the command line itself: it would end up in your shell history and in `ps`. + +In the CLI, a platform token only works for `nan run` and `nan runs` (see [API keys and tokens](#api-keys-and-tokens)). `nan me`, `nan metrics usage` and the dashboard still need a session from `nan auth login`. + +### Use the CLI in CI or on a server + +Create a [token](#api-keys-and-tokens), store it in your CI's secret store and expose it as `NAN_TOKEN`. For example, in GitHub Actions: + +```yaml +- name: Review the PR with an agent + env: + NAN_TOKEN: ${{ secrets.NAN_TOKEN }} + run: | + curl -fsSL https://nan.builders/install | bash + nan run --ws ci --timeout 20m -f .github/review-task.md +``` + +That step waits for the run and fails if the run fails. To start the run and move on without waiting, use `--detach` and allow for its exit code 75: + +```bash +nan run --ws ci --detach "upgrade the dependencies" || [ $? -eq 75 ] +``` + + +Store the token only as a secret (`secrets.NAN_TOKEN` in GitHub Actions), never in the repository or in the workflow file. It can start runs that spend your plan. + + +On a server you set up once, save it with `nan auth login --api-token` instead. + +## From the API + +The same runs are available over HTTP, at `https://api.nan.builders/v1/runs`, for your own scripts and integrations. + +### API keys and tokens + +Two kinds of credentials work with the runs API. Both are in your [settings on the portal](https://cloud.nan.builders/): + +| | **API Keys** | **Tokens** | +|---|---|---| +| Looks like | `sk-...` | `nan_pat_...` | +| Inference (models) | Yes | **No, never** | +| Platform API (runs, your workspaces) | Yes | Yes | +| Who can create them | Plans with inference | Any member with an active subscription | + +A **token** is the one to give a script that only manages runs. It cannot call the models directly, but it can start runs that spend your plan, so keep it as private as an API key and revoke it if it leaks. Create it under **Settings → Tokens**: + +- It is shown **once**, when you create it. Copy it then. +- You can have up to **10** active tokens. +- Each one expires after **30, 90 or 365 days**, or never. +- You can revoke any of them at any time. + +Creating runs still needs a plan with inference, whatever credential you use. + + +The key inside a workspace, the one its agents use for inference, cannot manage runs: the API answers it with `403`. An agent working in your workspace cannot start new runs on its own, unless you give it one of your own API keys or tokens. + + +### Create a run + +```bash +export NAN_TOKEN="nan_pat_..." + +curl https://api.nan.builders/v1/runs \ + -H "Authorization: Bearer $NAN_TOKEN" \ + -H "Content-Type: application/json" \ + -H "Idempotency-Key: fix-date-parser-1" \ + -d '{ + "workspace": "develop", + "agent": "pi", + "prompt": "add tests for the date parser and make them pass", + "cwd": "/home/nan/projects/api", + "git_isolation": null, + "timeout_seconds": 1800 + }' +``` + +| Field | Required | What it is | +|---|---|---| +| `workspace` | Yes | Name or id of the workspace. | +| `agent` | Yes | `pi` or `hermes`. | +| `prompt` | Yes | The task, up to 32 KiB. | +| `cwd` | No | The folder, `/home/nan` by default. | +| `git_isolation` | No | Separate branch: `null` for auto (the default), `true` for on, `false` for off. | +| `timeout_seconds` | No | The time limit, from 60 to 7200. 1800 (30 minutes) by default. | +| `model` | No | The model for the agent. Its own by default. | + +The fields match the portal form: **Agent** is `agent`, **Task** is `prompt`, **Folder** is `cwd`, **Separate branch** is `git_isolation` and **Time limit** is `timeout_seconds`. + +The answer is the run, with its `id` and its `state`. The `Idempotency-Key` header is optional: if a retry sends the same key with the same body, you get the run that was already created instead of a second one. + +### Follow it + +The events of a run come as a stream of server-sent events when you ask for `text/event-stream`. With `curl -N` you see them as they arrive: + +```bash +curl -N https://api.nan.builders/v1/runs/$RUN_ID/events \ + -H "Authorization: Bearer $NAN_TOKEN" \ + -H "Accept: text/event-stream" +``` + +Each frame is one of these: + +- `event: run_event`, with an `id:` line: one event of the log (a message, a tool call, its result...). Send the last `id` you saw as the `Last-Event-ID` header to resume after a dropped connection, without missing or repeating anything. +- `event: state`: the run changed state. +- `event: end`: the run is over and its log has been sent in full. The data is the finished run. The stream closes after it. + +Every 15 seconds the stream sends a comment line to keep the connection open. A stream lasts 30 minutes at most: if it closes before the `end`, reconnect with `Last-Event-ID` and carry on. Without the `Accept` header, the same endpoint answers pages of JSON instead: pass the `next_after` of each page as `?after=` to get the next one. + +To see how it ended: + +```bash +curl https://api.nan.builders/v1/runs/$RUN_ID \ + -H "Authorization: Bearer $NAN_TOKEN" +``` + +### Cancel it + +```bash +curl -X POST https://api.nan.builders/v1/runs/$RUN_ID/cancel \ + -H "Authorization: Bearer $NAN_TOKEN" +``` + +### List your runs + +```bash +curl "https://api.nan.builders/v1/runs?workspace=develop&state=running&limit=20" \ + -H "Authorization: Bearer $NAN_TOKEN" +``` + +All filters are optional. If there are more runs than fit in a page, the answer has a `next_cursor`: pass it as `?cursor=` to get the next page. + +### Errors + +Errors use the same format as the inference API: + +```json +{"error": {"message": "...", "type": "...", "param": null, "code": "tier_restricted"}} +``` + +| Status and code | What to do | +|---|---| +| `400 invalid_request` | A field is wrong. `param` says which one. | +| `403 tier_restricted` | Your plan does not include inference. Upgrade it to create runs. | +| `403 workspace_key_not_allowed` | You used the key from inside a workspace. Use an API key or a token. | +| `404` | The workspace or the run does not exist, or is not yours. | +| `409 idempotency_conflict` | You reused an `Idempotency-Key` with a different body. Use a new key. | +| `409 workspace_not_running` | Start the workspace first. | +| `409 agent_not_installed` | Install the agent from the **Software** panel, or pick the other one. | +| `409 no_inference_key` | The workspace has no inference key. Check that your plan includes inference. | +| `429 run_queue_full` | The queue is full (20 per workspace, 50 in total). Wait for some runs to end. | +| `429 rate_limited` | More than 60 runs in the last hour. Try again later. | + +## FAQ + +
+ +No. With a separate branch, the changes stay on a `nan-run/...` branch in the repository inside your workspace. Pushing, merging or throwing them away is up to you. + +
+ +
+ +Nothing: the run keeps going in the workspace. Open its page in the **Runs** tab, or run `nan runs logs -f`, to pick it up again. + +
+ +
+ +No. Runs use the models through your workspace's inference key, so creating them needs a plan with inference. You can still create a token and list your workspaces and runs. + +
+ +
+ +No. Each run has its own share of the workspace's CPU and memory. If one gets out of hand, only that run is stopped. + +
+ +
+ +Write in `#support` on Discord. + +
diff --git a/src/content/docs/workspaces.mdx b/src/content/docs/workspaces.mdx index 548611d..1f74a54 100644 --- a/src/content/docs/workspaces.mdx +++ b/src/content/docs/workspaces.mdx @@ -215,6 +215,12 @@ gentle-shell is an add-on for Pi (memory between sessions with Engram), so it ne On an inference plan, the agents come set up to use NaN models with your membership. The setup guides for [Pi](/docs/pi) and [Hermes](/docs/hermes) explain how to change the model or the provider by hand. +## Run agents on tasks + +You do not have to stay connected while an agent works. A **run** gives Pi or Hermes a task inside your workspace and lets it work on its own: you follow its log live, it keeps going when you close the page, and in a git repository its changes land on a branch for you to review. Start one from the **Runs** tab of the workspace, from your terminal with `nan run`, or from the API. + +How it works, the limits and the commands are in [Agent runs](/docs/runs). + ## Backups Workspaces have **no backups** unless you turn on the **backup add-on** for that workspace, from its **Backups** tab. diff --git a/src/lib/mdxToText.ts b/src/lib/mdxToText.ts index b9bb74e..0a9e5fa 100644 --- a/src/lib/mdxToText.ts +++ b/src/lib/mdxToText.ts @@ -528,6 +528,11 @@ function isMdxExpression(t: string): boolean { return t === 'mdxFlowExpression' || t === 'mdxTextExpression'; } +/** True for an expression that is a single block comment and nothing else. */ +export function isCommentOnly(raw: string): boolean { + return /^\/\*[\s\S]*\*\/$/.test(raw) && raw.indexOf('*/') === raw.length - 2; +} + function promoteHeadingParagraphs(root: { children?: unknown[] }): void { if (!Array.isArray(root.children)) return; const out: unknown[] = []; @@ -573,6 +578,9 @@ function transformTree(root: { children?: unknown[] }, rateLimits: RateLimitsCon if (isMdxExpression(child.type)) { const raw = ((child as unknown as { value?: string }).value ?? '').trim(); + // An MDX comment (`{/* ... */}`) renders nothing, so it serves nothing. + // Anything else in braces is still refused: it would be code we cannot run. + if (isCommentOnly(raw)) continue; throw new Error(`Unexpected MDX expression: {${raw}}`); } diff --git a/src/tests/layouts/runsDocs.test.ts b/src/tests/layouts/runsDocs.test.ts new file mode 100644 index 0000000..4ac44ef --- /dev/null +++ b/src/tests/layouts/runsDocs.test.ts @@ -0,0 +1,361 @@ +import { describe, expect, test } from 'vitest'; +import { existsSync, readFileSync, readdirSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, resolve } from 'node:path'; +import { parseFrontmatter } from '@astrojs/markdown-remark'; +import { mdxToText } from '../../lib/mdxToText'; + +/** + * The Agent runs guide: what members read before they start their first run, + * from the portal, `nan run` or the API. It opens together with Agent + * Automations (Phase 2). + * + * The facts pinned here come from the Phase 1 spec + * (devops AGENT-AUTOMATIONS-PHASE1.md: §2.1.3 API, §2.1.4 admission, §2.2.6 + * worktrees, §2.4 CLI) and the production E2E of 2026-10-10: per-size + * concurrency, the per-member cap shared with inference, the queue limits, the + * CLI exit codes, and the two kinds of credentials (API keys vs `nan_pat_` + * tokens). + */ +const here = dirname(fileURLToPath(import.meta.url)); +const src = resolve(here, '../..'); +const read = (p: string) => readFileSync(resolve(src, p), 'utf-8'); + +const pages = { + en: { dir: 'content/docs/', prefix: '', group: 'Guides', title: 'Agent runs' }, + es: { dir: 'content/docs-es/', prefix: '/es', group: 'Guías', title: 'Runs de agentes' }, +} as const; + +/** Workspace size -> runs at once. */ +const CONCURRENCY: [string, number][] = [ + ['Micro', 1], + ['Nano', 2], + ['Basic', 3], + ['Medium', 5], + ['Large', 8], +]; + +const EXIT_CODES = ['0', '1', '2', '3', '4', '64', '65', '69', '75']; + +const INTERNAL = /firecracker|micro-?vms?\b|\bvm\b|\bkvm\b|kubernetes|\bk8s\b|\br2\b|nan-vmd|systemd|litellm|cgroup/i; + +const pub = resolve(src, '../public'); +/** The portal captures, in the order the guide shows them. */ +const SHOTS = ['runs-01-tab', 'runs-02-new-run', 'runs-03-run-page']; +/** Things that must never be published: full run or workspace ids (UUIDs) and private addresses. */ +const PRIVATE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}|\b10\.\d{1,3}\.\d{1,3}\.\d{1,3}\b|\bws-[0-9a-f]{8}\b/i; + +interface Shot { + src: string; + alt: string; + caption: string; + width: number; + height: number; +} + +/** Every of a page, in order. */ +function screenshots(body: string): Shot[] { + return [...body.matchAll(/]*?)\/>/g)].map((m) => { + const attr = (name: string) => new RegExp(`${name}="([^"]*)"`).exec(m[1])?.[1] ?? ''; + const num = (name: string) => Number(new RegExp(`${name}=\\{(\\d+)\\}`).exec(m[1])?.[1]); + return { src: attr('src'), alt: attr('alt'), caption: attr('caption'), width: num('width'), height: num('height') }; + }); +} + +/** Pixel size of a WebP (lossy VP8, lossless VP8L or extended VP8X). */ +function webpSize(file: string): { width: number; height: number } { + const b = readFileSync(file); + expect(b.toString('ascii', 0, 4)).toBe('RIFF'); + expect(b.toString('ascii', 8, 12)).toBe('WEBP'); + const chunk = b.toString('ascii', 12, 16); + if (chunk === 'VP8 ') return { width: b.readUInt16LE(26) & 0x3fff, height: b.readUInt16LE(28) & 0x3fff }; + if (chunk === 'VP8L') { + const bits = b.readUInt32LE(21); + return { width: (bits & 0x3fff) + 1, height: ((bits >> 14) & 0x3fff) + 1 }; + } + return { width: b.readUIntLE(24, 3) + 1, height: b.readUIntLE(27, 3) + 1 }; +} + +const strip = (raw: string) => raw.replace(/^---[\s\S]*?\n---\n/, ''); + +/** + * Heading ids as the site renders them. Astro slugs headings with + * github-slugger: lowercase, punctuation dropped, accents kept, spaces to + * hyphens. The build output was checked against this for both pages. + */ +const slug = (h: string) => + h + .toLowerCase() + .replace(/[^\p{L}\p{N}\s_-]/gu, '') + .replace(/ /g, '-'); + +function headingIds(body: string): Set { + const ids = new Set(); + let inFence = false; + for (const line of body.split('\n')) { + if (line.startsWith('```')) inFence = !inFence; + const m = !inFence && /^#{1,6}\s+(.*)$/.exec(line); + if (m) ids.add(slug(m[1])); + } + return ids; +} + +describe.each(Object.entries(pages))('agent runs guide (%s)', (locale, page) => { + const raw = read(page.dir + 'runs.mdx'); + const fm = parseFrontmatter(raw).frontmatter as { order?: number; group?: string; title?: string }; + const body = strip(raw); + const flat = body.replace(/\s+/g, ' '); + + test('sits in the guides group, right after Workspaces', () => { + expect(fm.title).toBe(page.title); + expect(fm.group).toBe(page.group); + const ws = parseFrontmatter(read(page.dir + 'workspaces.mdx')).frontmatter as { order: number }; + expect(fm.order).toBe(ws.order + 1); + }); + + test('names both agents and the three ways in', () => { + expect(flat).toContain('Pi'); + expect(flat).toContain('Hermes'); + expect(body).toContain('**Runs**'); + expect(body).toContain('**New run**'); + expect(body).toContain('nan run'); + expect(body).toContain('https://api.nan.builders/v1/runs'); + }); + + test('uses the labels the portal shows', () => { + for (const label of ['Agent runs', 'New run', 'Agent', 'Task', 'Folder', 'Separate branch', 'Auto', 'On', 'Off', 'Time limit', 'Start run', 'Cancel run', 'Output', 'Tool call', 'Tool result']) { + expect(body, label).toContain(`**${label}**`); + } + // The old names of the form fields are gone from the portal copy. + expect(body).not.toMatch(/\| (Prompt|Working directory|Worktree|Timeout) \|/); + }); + + test('maps the portal labels to the CLI flags and the API fields', () => { + expect(flat).toMatch(/\*\*Agent\*\* (is|es) `--agent`, \*\*Folder\*\* (is|es) `--cwd`, \*\*Separate branch\*\* (is|es) `--worktree` \/ `--no-worktree` (and|y) \*\*Time limit\*\* (is|es) `--timeout`/); + expect(flat).toMatch(/\*\*Agent\*\* (is|es) `agent`, \*\*Task\*\* (is|es) `prompt`, \*\*Folder\*\* (is|es) `cwd`, \*\*Separate branch\*\* (is|es) `git_isolation` (and|y) \*\*Time limit\*\* (is|es) `timeout_seconds`/); + }); + + test('describes the portal form with its limits', () => { + expect(flat).toContain('32 KiB'); + expect(body).toContain('`/home/nan`'); + // cloud-ui src/lib/runs.ts: DEFAULT_TIMEOUT_SECONDS = 1800, options 15 min / 30 min / 1 hour / 2 hours. + expect(flat).toMatch(/\| \*\*Time limit\*\* \|[^|]*\*\*30 min\*\* (by default|por defecto), (the same as|igual que en) (the CLI and the API|la CLI y la API)/); + expect(flat).toMatch(/15 min, 30 min, 1 hour (and|y) 2 hours/); + expect(flat).toMatch(/15 min, 30 min, 1 (hour|hora) (and|y) 2 (hours|horas), (with 30 min by default|con 30 min por defecto)/); + expect(flat).not.toMatch(/60 (or|o|and|y) 120 (minutes|minutos)/); + for (const tab of ['Overview', 'Console', 'SSH keys', 'Events', 'Backups']) expect(body).toContain(`**${tab}**`); + }); + + test('explains the worktree rules: branch name, never pushes, no-change branch deleted', () => { + expect(body).toContain('`nan-run/6f1c2a9b`'); + expect(flat).toMatch(/8 (characters|primeros caracteres)/); + expect(flat).toMatch(/\*\*(never pushes|nunca hace push)\*\*/); + expect(flat).toMatch(/(the branch is deleted|la rama se borra)/); + expect(flat).toMatch(/(fails with a configuration error|falla con un error de configuración)/); + }); + + test('lists every run state', () => { + for (const s of ['queued', 'starting', 'running', 'succeeded', 'failed', 'cancelled', 'timed_out']) { + expect(body).toContain(`\`${s}\``); + } + expect(flat).toMatch(/60 (seconds|segundos)/); + expect(flat).toMatch(/2 (hours|horas)/); + }); + + test('publishes the concurrency per size and the per-member caps', () => { + for (const [size, n] of CONCURRENCY) { + expect(body, size).toMatch(new RegExp(`^\\| ${size}\\b[^|]*\\| ${n} \\|$`, 'm')); + } + expect(flat).toMatch(/\*\*5 runs/); + expect(flat).toMatch(/\*\*8 (on|con) Premium\*\*/); + expect(flat).toMatch(/\*\*20 runs (per|por) workspace\*\*/); + expect(flat).toMatch(/\*\*50 (in total|en total)\*\*/); + expect(flat).toMatch(/\*\*60 runs (per hour|por hora)\*\*/); + }); + + test('says runs need inference, count as usage and mask secrets', () => { + expect(body).toContain('tier_restricted'); + expect(body).toContain('`***`'); + }); + + test('documents the CLI: version, flags, Ctrl-C and every exit code', () => { + expect(body).toContain('**0.1.25**'); + expect(body).not.toContain('0.1.24'); + expect(body).toContain('`nan --version` '); + expect(body).toContain('curl -fsSL https://nan.builders/install | bash'); + expect(body).toContain('nan auth login'); + for (const flag of ['--ws', '--agent', '--model', '--cwd', '--worktree', '--no-worktree', '--timeout', '--detach', '--json', '--idempotency-key', '-f', '--token-file']) { + expect(body, flag).toContain(`\`${flag}`); + } + for (const cmd of ['nan runs ls', 'nan runs show', 'nan runs logs -f', 'nan runs cancel', 'nan runs logs --json']) { + expect(body, cmd).toContain(cmd); + } + for (const code of EXIT_CODES) expect(body, code).toMatch(new RegExp(`^\\| ${code} \\|`, 'm')); + expect(flat).toMatch(/2 (seconds|segundos)/); + }); + + test('documents the API with working curl examples: create, follow, cancel', () => { + expect(body).toContain('-H "Idempotency-Key:'); + expect(body).toContain('curl -N https://api.nan.builders/v1/runs/$RUN_ID/events'); + expect(body).toContain('-H "Accept: text/event-stream"'); + expect(body).toContain('curl -X POST https://api.nan.builders/v1/runs/$RUN_ID/cancel'); + // One table, rows back to back: a paragraph between two rows splits it and + // turns the rest into loose text with pipes. + const rows = ['workspace', 'agent', 'prompt', 'cwd', 'git_isolation', 'timeout_seconds', 'model']; + const table = rows.map((f) => `\\| \`${f}\` \\|[^\\n]*`).join('\\n'); + expect(body).toMatch(new RegExp(table)); + for (const frame of ['event: run_event', 'event: state', 'event: end']) expect(body).toContain(frame); + expect(body).toContain('Last-Event-ID'); + expect(body).toContain('next_cursor'); + expect(body).toContain('"error": {"message"'); + for (const code of ['400 invalid_request', '404', '409 idempotency_conflict', '429 run_queue_full', '429 rate_limited']) expect(body, code).toContain(`| \`${code}\` |`); + }); + + test('the create example is valid JSON with the documented fields', () => { + const json = /-d '(\{[\s\S]*?\})'/.exec(body)![1]; + const req = JSON.parse(json); + expect(Object.keys(req).sort()).toEqual(['agent', 'cwd', 'git_isolation', 'prompt', 'timeout_seconds', 'workspace']); + expect(req.timeout_seconds).toBeGreaterThanOrEqual(60); + expect(req.timeout_seconds).toBeLessThanOrEqual(7200); + }); + + test('explains API keys vs tokens', () => { + expect(body).toContain('`nan_pat_...`'); + expect(body).toContain('**Settings → Tokens**'); + expect(flat).toMatch(/\*\*(No, never|No, nunca)\*\*/); + expect(flat).toMatch(/\*\*10\*\*/); + expect(flat).toMatch(/\*\*30, 90 (or|o) 365/); + expect(body).toContain('workspace_key_not_allowed'); + expect(flat).toMatch(/(spend your plan|gastan tu plan)/); + }); + + test('never shows a real-looking secret', () => { + expect(body).not.toMatch(/sk-[A-Za-z0-9]{8,}/); + expect(body).not.toMatch(/nan_pat_[A-Za-z0-9]{4,}/); + }); + + test('keeps infrastructure words out of member copy', () => { + expect(body).not.toMatch(INTERNAL); + }); + + test('in-page anchors point at headings that exist', () => { + const ids = headingIds(body); + const anchors = [...body.matchAll(/\]\(#([^)]+)\)/g)].map((m) => m[1]); + expect(anchors.length).toBeGreaterThan(0); + for (const a of anchors) expect(ids.has(a), a).toBe(true); + }); + + test('links only to docs pages that exist in the same locale', () => { + const slugs = [...body.matchAll(new RegExp(`\\(${page.prefix}/docs/([\\w-]+)\\)`, 'g'))].map((m) => m[1]); + expect(slugs).toEqual(expect.arrayContaining(['workspaces', 'nan-cli'])); + for (const slug of slugs) { + expect(existsSync(resolve(src, page.dir, `${slug}.md`)) || existsSync(resolve(src, page.dir, `${slug}.mdx`)), slug).toBe(true); + } + }); + + test('shows the three portal captures, in order, and no placeholder is left', () => { + expect(screenshots(body).map((s) => s.src)).toEqual(SHOTS.map((n) => `/docs/runs/${n}.webp`)); + expect(body).not.toContain('SCREENSHOT'); + }); + + test.each(SHOTS)('screenshot %s: file exists, size matches, alt and caption present', (name) => { + const shot = screenshots(body).find((s) => s.src === `/docs/runs/${name}.webp`)!; + const file = resolve(pub, `docs/runs/${name}.webp`); + expect(existsSync(file)).toBe(true); + expect(webpSize(file)).toEqual({ width: shot.width, height: shot.height }); + expect(shot.alt.length).toBeGreaterThan(30); + expect(shot.caption.length).toBeGreaterThan(5); + expect(`${shot.src} ${shot.alt} ${shot.caption}`).not.toMatch(PRIVATE); + expect(`${shot.alt} ${shot.caption}`).not.toMatch(INTERNAL); + }); + + test('is served by /api/docs without errors, and the placeholders are not', async () => { + const text = await mdxToText(body); + expect(text).toContain(page.title); + expect(text).toContain('/docs/runs/runs-01-tab.webp'); + }); + + test('no em-dashes in the copy', () => { + expect(body).not.toContain('—'); + }); + + test('Workspaces, the CLI guide and the introduction link to it', () => { + const link = `(${page.prefix}/docs/runs)`; + for (const f of ['workspaces.mdx', 'nan-cli.mdx', 'intro.md']) expect(read(page.dir + f), f).toContain(link); + }); +}); + +describe('agent runs guide, both locales', () => { + test('the Spanish page is a translation with the same structure', () => { + const [en, es] = [pages.en, pages.es].map((p) => strip(read(p.dir + 'runs.mdx'))); + const shape = (b: string) => (b.match(/^#{1,3} /gm) ?? []).join(''); + expect(shape(es)).toEqual(shape(en)); + expect(es).not.toEqual(en); + const count = (b: string, re: RegExp) => (b.match(re) ?? []).length; + expect(count(es, /```bash/g)).toBe(count(en, /```bash/g)); + expect(count(es, /
{ + const [en, es] = [pages.en, pages.es].map((p) => screenshots(strip(read(p.dir + 'runs.mdx')))); + expect(es.map((s) => s.src)).toEqual(en.map((s) => s.src)); + en.forEach((shot, i) => expect(es[i].alt).not.toBe(shot.alt)); + }); + + test('only the published screenshots live in the folder (no raw captures)', () => { + expect(readdirSync(resolve(pub, 'docs/runs')).sort()).toEqual(SHOTS.map((n) => `${n}.webp`).sort()); + }); + +}); + +describe.each(Object.entries(pages))('CLI authentication for runs (%s)', (locale, page) => { + const body = strip(read(page.dir + 'runs.mdx')); + const flat = body.replace(/\s+/g, ' '); + + test('lists the sources in the order the CLI (v0.1.25) uses them', () => { + const list = /^1\. \*\*`NAN_TOKEN`\*\*[\s\S]*?^5\. [^\n]*$/m.exec(body)![0]; + const order = [ + '1. **`NAN_TOKEN`**', + '2. **`--token-file PATH`**', + locale === 'en' ? '3. **A saved token**, from `nan auth login --api-token`' : '3. **Un token guardado**, con `nan auth login --api-token`', + locale === 'en' ? '4. **Your session**, from `nan auth login`' : '4. **Tu sesión**, de `nan auth login`', + locale === 'en' ? '5. **A saved API key**' : '5. **Una API key guardada**', + ]; + const at = order.map((o) => list.indexOf(o)); + at.forEach((i, n) => expect(i, order[n]).toBeGreaterThan(-1)); + expect([...at].sort((a, b) => a - b)).toEqual(at); + }); + + test('states the safety rules of each source', () => { + expect(flat).toMatch(/(never written to disk|nunca se escribe en disco)/); + expect(body).toContain('`chmod 600`'); + expect(body).toContain('`0600`'); + expect(body).toContain('nan auth login --api-token < token.txt'); + expect(flat).toMatch(/(never copies a platform token|nunca copia un token de plataforma)/); + expect(flat).toMatch(/(shell history|historial de tu shell)/); + }); + + test('says platform tokens only work for runs, not for nan me / metrics / the dashboard', () => { + expect(flat).toContain('`nan me`, `nan metrics usage`'); + }); + + test('shows the GitHub Actions example from the CLI README, with a secret and a warning', () => { + expect(body).toContain('NAN_TOKEN: ${{ secrets.NAN_TOKEN }}'); + expect(body).toContain('nan run --ws ci --timeout 20m -f .github/review-task.md'); + expect(body).toMatch(/nan run --ws ci --detach "[^"]+" \|\| \[ \$\? -eq 75 \]/); + expect(body).toMatch(//); + }); + + test('lists the main event types of nan runs logs --json', () => { + for (const t of ['started', 'message', 'tool_call', 'tool_result', 'log', 'usage', 'error', 'finished']) { + expect(body, t).toContain(`\`${t}\``); + } + }); + + test('the NaN CLI guide points at v0.1.25 and the token options', () => { + const cli = read(page.dir + 'nan-cli.mdx'); + expect(cli).toContain('v0.1.25'); + expect(cli).not.toContain('v0.1.24'); + expect(cli).toContain('`NAN_TOKEN`, `--token-file` o'.replace(' o', locale === 'en' ? ' or' : ' o')); + }); +}); diff --git a/src/tests/lib/mdxToTextComments.test.ts b/src/tests/lib/mdxToTextComments.test.ts new file mode 100644 index 0000000..c64d5bf --- /dev/null +++ b/src/tests/lib/mdxToTextComments.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, test } from 'vitest'; +import { isCommentOnly, mdxToText } from '../../lib/mdxToText'; + +/** + * Docs pages carry `{/* SCREENSHOT: ... *\/}` placeholders where a capture is + * still to come. The extractor behind /api/docs refused every MDX expression, + * so one such comment would have taken the whole page down. A comment renders + * nothing, so it must serve nothing; any other expression is still refused. + */ +describe('MDX comments in the docs text extractor', () => { + test('a block comment on its own line is dropped', async () => { + const text = await mdxToText('# Title\n\n{/* SCREENSHOT: runs tab */}\n\nAfter.\n'); + expect(text).toContain('After.'); + expect(text).not.toContain('SCREENSHOT'); + }); + + test('a multi-line comment is dropped too', async () => { + const text = await mdxToText('Before.\n\n{/*\n TODO: picture\n*/}\n\nAfter.\n'); + expect(text).not.toContain('TODO'); + expect(text).toContain('Before.'); + }); + + test('any other expression is still refused', async () => { + await expect(mdxToText('Value: {1 + 1}\n')).rejects.toThrow(/Unexpected MDX expression/); + await expect(mdxToText('{/* a */ 1}\n')).rejects.toThrow(/Unexpected MDX expression/); + }); + + test('isCommentOnly accepts a single comment and nothing else', () => { + expect(isCommentOnly('/* x */')).toBe(true); + expect(isCommentOnly('/* a */ + /* b */')).toBe(false); + expect(isCommentOnly('x')).toBe(false); + }); +});