Ghosty
API

Eventos (SSE)

Catálogo de los eventos del stream de una conversación — started, chunk, tool, permission, artifact, done — y cómo escribir un cliente que no pierda nada.

Actualizado 2026-09-16

GET /api/v2/me/agents/:agentId/conversations/:sessionId/events es un stream Server-Sent Events con eventos con nombre (event: chunk), y siempre con data (aunque sea {}).

Tres reglas que sorprenden

  1. Es re-suscribible. Al conectarte recibes primero todo lo que el turno en curso ya emitió, y luego el directo. Un F5, o volver mañana, no pierde nada. Desconectarte no cancela el turno: el trabajo es del servidor.
  2. En reposo llega un done de entrada, marcado reposo: true, para que distingas «no hay turno» de «acabó un turno».
  3. Los permisos abiertos se reenvían en cada suscripción. El servidor no sabe qué pantallas siguen vivas. Retíralos con permission-resolved, que además cubre que conteste otro cliente.

Hay un latido cada 25 s para que ningún proxy corte un turno largo por silencio.

EventodataCuándo
started{}Conexión establecida.
caps{ image: boolean }Capacidades del agente (si ve imágenes).
status{ phase: "waking" | "session" }La máquina está despertando / el turno está en sesión.
chunk{ text, turnId }Un pedazo de la respuesta. Concaténalos por turnId.
thought{ text }Razonamiento visible del modelo, si el motor lo expone.
tool{ id, title?, kind?, status, path?, detalle? }Ciclo de vida de una herramienta: pending → in_progress → completed | failed. Mismo id en cada cambio.
title{ title }La conversación fue bautizada.
models{ options: [{value,name}], current }Modelos disponibles y el actual.
usage{ used, size, cost, input?, output? }Tokens acumulados de la sesión. used puede superar size tras una compactación: no es un medidor de contexto.
permission{ id, title, tool?, options: [{optionId,name,kind?}] }El agente pide confirmación. Contesta con POST …/permission { id, optionId }. Diez minutos sin respuesta = denegado.
permission-resolved{ id }Ese pedido ya se cerró (por ti, por otro cliente o por plazo).
artifact{ … }El agente entregó algo: archivo, documento, hoja. Lleva url firmada y tipo.
done{ turnId } o { reposo: true }Terminó el turno / no hay turno.
error{ message }El turno falló. El texto parcial recibido sigue siendo válido.

Cliente mínimo en Node

typescript
const res = await fetch(url, { headers: { Authorization: `Bearer ${token}`, Accept: "text/event-stream" } });const reader = res.body!.pipeThrough(new TextDecoderStream()).getReader();let buf = "";for (;;) {  const { value, done } = await reader.read();  if (done) break;  buf += value;  let i;  while ((i = buf.indexOf("\n\n")) !== -1) {    const frame = buf.slice(0, i); buf = buf.slice(i + 2);    const event = /^event: (.*)$/m.exec(frame)?.[1];    const data = /^data: (.*)$/m.exec(frame)?.[1];    if (!event || data == null) continue;    handle(event, JSON.parse(data));  }}
python
import json, requestswith requests.get(url, headers={"Authorization": f"Bearer {token}"}, stream=True) as r:    event = None    for line in r.iter_lines(decode_unicode=True):        if line.startswith("event: "): event = line[7:]        elif line.startswith("data: ") and event:            handle(event, json.loads(line[6:])); event = None

Reintentos

Si la conexión se corta, vuelve a abrirla: recibes el replay y sigues. Para saber si el turno terminó mientras estabas fuera, mira ultimoTurno en GET …/conversations/:sessionId.