eigenatlas — explainers
00 / 07 preludio
EIGEN EXPLAINERS · 01 · tier 6 — agent stack

/agents-sdk

El plumbing del agente, escrito por Cloudflare. Tú escribes la lógica.

tiempo de lectura
~14 MIN
contiene
DEMO LIVE
nivel
INTERMEDIO
scroll ↓
acto i el problema

Antes de Agents SDK, escribías esto.

conversation-agent.ts
durable object · sin agents sdk
// conversation-agent.ts — sin Agents SDKimport { DurableObject } from "cloudflare:workers";interface Message {  id: string;  role: "user" | "assistant";  content: string;  ts: number;}interface AgentState {  status: "idle" | "thinking" | "responding";  messageCount: number;  lastActivity: number;}export class ConversationDO extends DurableObject {  private wsClients = new Set<WebSocket>();  private state: AgentState | null = null;  constructor(ctx: DurableObjectState, env: Env) {    super(ctx, env);    ctx.blockConcurrencyWhile(async () => {      ctx.storage.sql.exec(`        CREATE TABLE IF NOT EXISTS messages (          id TEXT PRIMARY KEY,          role TEXT NOT NULL,          content TEXT NOT NULL,          ts INTEGER NOT NULL        );        CREATE TABLE IF NOT EXISTS state_kv (          key TEXT PRIMARY KEY,          value TEXT NOT NULL        );      `);      this.state = await this.loadState();    });    for (const ws of ctx.getWebSockets()) {      this.wsClients.add(ws);    }  }  async fetch(request: Request): Promise<Response> {    const url = new URL(request.url);    if (request.headers.get("Upgrade") === "websocket") {      const pair = new WebSocketPair();      this.ctx.acceptWebSocket(pair[1]);      this.wsClients.add(pair[1]);      return new Response(null, { status: 101, webSocket: pair[0] });    }    if (url.pathname === "/messages" && request.method === "GET") {      return Response.json(this.getMessages());    }    if (url.pathname === "/send" && request.method === "POST") {      const { content } = await request.json<{ content: string }>();      return Response.json(await this.handleMessage(content));    }    if (url.pathname === "/state") {      return Response.json(this.state);    }    return new Response("Not found", { status: 404 });  }  async webSocketMessage(ws: WebSocket, msg: string | ArrayBuffer) {    if (typeof msg !== "string") return;    const parsed = JSON.parse(msg);    if (parsed.type === "send") {      await this.handleMessage(parsed.content);    }  }  async webSocketClose(ws: WebSocket) {    this.wsClients.delete(ws);  }  async alarm() {    const next = await this.ctx.storage.get<string>("next_action");    if (next === "follow_up") await this.sendFollowUp();    if (next === "cleanup") await this.cleanup();  }  async scheduleFollowUp(delayMs: number) {    await this.ctx.storage.put("next_action", "follow_up");    await this.ctx.storage.setAlarm(Date.now() + delayMs);  }  private async loadState(): Promise<AgentState> {    const row = this.ctx.storage.sql      .exec<{ value: string }>(        "SELECT value FROM state_kv WHERE key = ?", "state"      ).one();    return row      ? JSON.parse(row.value)      : { status: "idle", messageCount: 0, lastActivity: Date.now() };  }  private async setState(state: AgentState) {    this.state = state;    this.ctx.storage.sql.exec(      "INSERT OR REPLACE INTO state_kv (key, value) VALUES (?, ?)",      "state", JSON.stringify(state),    );    this.broadcast({ type: "state", state });  }  private broadcast(msg: unknown) {    const json = JSON.stringify(msg);    for (const ws of this.wsClients) {      try { ws.send(json); } catch {}    }  }  // ⬇  TU LÓGICA — 6 líneas en 127  private async handleMessage(content: string) {    const id = crypto.randomUUID();    this.ctx.storage.sql.exec(      "INSERT INTO messages (id, role, content, ts) VALUES (?, ?, ?, ?)",      id, "user", content, Date.now(),    );    await this.setState({ ...this.state!, messageCount: this.state!.messageCount + 1, lastActivity: Date.now() });    return { id, ok: true };  }
L 04—15 · tipos
Tipos a mano. Necesarios porque vas a hablar contigo mismo, con el cliente, con tu propia DB.
L 21—43 · setup del do
Constructor + migrations SQL + rehydration de WebSockets. Cada DO nuevo lo escribe igual.
L 45—67 · routing
URL parsing y WebSocket upgrade a mano. Cada endpoint, una rama del if.
L 69—79 · websocket lifecycle
Handlers de message + close. Mantienes el set de clients tú mismo.
L 81—90 · scheduling
Alarm + setAlarm + key intermedia para saber qué disparar. Una sola alarma por DO, multiplexada por convención.
L 92—116 · estado
Load + save + broadcast. Serialización JSON. Try/catch en cada send. Es el plumbing del plumbing.
L 118—127 · tu lógica
Esto es lo único que tu app hace de verdad. 6 líneas. Las otras 121 son ceremonia.
líneas
127
plumbing
0%
negocio
0%
acto ii la solución

Después, escribes esto.

conversation-agent.ts
antes · 127 líneas
// conversation-agent.ts — sin Agents SDKimport { DurableObject } from "cloudflare:workers";interface Message {  id: string;  role: "user" | "assistant";  content: string;  ts: number;}interface AgentState {  status: "idle" | "thinking" | "responding";  messageCount: number;  lastActivity: number;}export class ConversationDO extends DurableObject {  private wsClients = new Set<WebSocket>();  private state: AgentState | null = null;  constructor(ctx: DurableObjectState, env: Env) {    super(ctx, env);    ctx.blockConcurrencyWhile(async () => {      ctx.storage.sql.exec(`        CREATE TABLE IF NOT EXISTS messages (          id TEXT PRIMARY KEY,          role TEXT NOT NULL,          content TEXT NOT NULL,          ts INTEGER NOT NULL        );        CREATE TABLE IF NOT EXISTS state_kv (          key TEXT PRIMARY KEY,          value TEXT NOT NULL        );      `);      this.state = await this.loadState();    });    for (const ws of ctx.getWebSockets()) {      this.wsClients.add(ws);    }  }  async fetch(request: Request): Promise<Response> {    const url = new URL(request.url);    if (request.headers.get("Upgrade") === "websocket") {      const pair = new WebSocketPair();      this.ctx.acceptWebSocket(pair[1]);      this.wsClients.add(pair[1]);      return new Response(null, { status: 101, webSocket: pair[0] });    }    if (url.pathname === "/messages" && request.method === "GET") {      return Response.json(this.getMessages());    }    if (url.pathname === "/send" && request.method === "POST") {      const { content } = await request.json<{ content: string }>();      return Response.json(await this.handleMessage(content));    }    if (url.pathname === "/state") {      return Response.json(this.state);    }    return new Response("Not found", { status: 404 });  }  async webSocketMessage(ws: WebSocket, msg: string | ArrayBuffer) {    if (typeof msg !== "string") return;    const parsed = JSON.parse(msg);    if (parsed.type === "send") {      await this.handleMessage(parsed.content);    }  }  async webSocketClose(ws: WebSocket) {    this.wsClients.delete(ws);  }  async alarm() {    const next = await this.ctx.storage.get<string>("next_action");    if (next === "follow_up") await this.sendFollowUp();    if (next === "cleanup") await this.cleanup();  }  async scheduleFollowUp(delayMs: number) {    await this.ctx.storage.put("next_action", "follow_up");    await this.ctx.storage.setAlarm(Date.now() + delayMs);  }  private async loadState(): Promise<AgentState> {    const row = this.ctx.storage.sql      .exec<{ value: string }>(        "SELECT value FROM state_kv WHERE key = ?", "state"      ).one();    return row      ? JSON.parse(row.value)      : { status: "idle", messageCount: 0, lastActivity: Date.now() };  }  private async setState(state: AgentState) {    this.state = state;    this.ctx.storage.sql.exec(      "INSERT OR REPLACE INTO state_kv (key, value) VALUES (?, ?)",      "state", JSON.stringify(state),    );    this.broadcast({ type: "state", state });  }  private broadcast(msg: unknown) {    const json = JSON.stringify(msg);    for (const ws of this.wsClients) {      try { ws.send(json); } catch {}    }  }  // tu lógica — los survivors  private async handleMessage(content: string) {    const id = crypto.randomUUID();    this.ctx.storage.sql.exec(      "INSERT INTO messages (...) VALUES (...)",      id, "user", content, Date.now(),    );    await this.setState({ ...this.state!, messageCount: this.state!.messageCount + 1 });    return { id, ok: true };  }
conversation-agent.ts
después · agents sdk
// conversation-agent.ts — con Agents SDKimport { Agent } from "agents";interface AgentState {  status: "idle" | "thinking" | "responding";  messageCount: number;  lastActivity: number;}export class ConversationAgent extends Agent<Env, AgentState> {  initialState: AgentState = {    status: "idle",    messageCount: 0,    lastActivity: Date.now(),  };  async onMessage(connection, message) {    const { content } = JSON.parse(message);    this.sql`INSERT INTO messages (id, role, content, ts)             VALUES (${crypto.randomUUID()}, 'user', ${content}, ${Date.now()})`;    this.setState({      ...this.state,      messageCount: this.state.messageCount + 1,      lastActivity: Date.now(),    });  }  async followUp() { /* tu lógica de follow-up */ }}
127 127 líneas
acto iii state + schedule

el agente se programa a sí mismo.

state.counter
0
incrementCounter
tareas en cola
0
this.getSchedules()
disparados
0
total fires
conversation-agent.ts
scheduling primitives
// dentro de tu Agent class:await this.schedule(5, 'incrementCounter');await this.schedule(10, 'logTick', {  msg: 'check status'});// cron syntax para tareas recurrentes:await this.schedule('0 0 * * *', 'cleanup');// fecha específica:await this.schedule(  new Date('2026-12-25T00:00:00Z'),  'sendGreeting',  { msg: 'feliz navidad' },);// las schedules viven en el SQLite del DO:// SELECT * FROM cf_agents_schedules// state también es persistente y reactivo:this.setState({  ...this.state,  counter: this.state.counter + 1,});

Las schedules viven en el SQLite del DO. Si el Worker reinicia, persisten. El alarm system de Cloudflare las dispara incluso si nadie está hablando con el agente. Reemplazan al cron + DB compartida del stack viejo.

agente live · simulado
id: shared

        
ahora +30s
    acto iv websockets + react

    State sync. Sin redux. Sin trpc. Sin polling.

    El mismo objeto this.state que el agente muta en el server, lo lee tu componente React. Las dos partes siempre están sincronizadas porque son la misma cosa, no dos copias.

    conversation-agent.ts
    server · agent class
    import { Agent } from "agents";export class ConversationAgent extends Agent<Env, State> {  initialState = { counter: 0, msgs: [] };  async onMessage(connection, msg) {    const { from, body } = JSON.parse(msg);    this.setState({      ...this.state,      counter: this.state.counter + 1,      msgs: [...this.state.msgs, { from, body, ts: Date.now() }],    });    // setState propaga a TODOS los clients via WebSocket  }}
    ChatUI.tsx
    client · react component
    import { useAgent } from "agents/react";function ChatUI({ id }) {  const { state, send } = useAgent({    agent: "conversation-agent",    name: id,  });  return (    <div>      <h1>{state.counter}</h1>      {state.msgs.map(m => <li>{m.body}</li>)}      <button onClick={() => send(...)}/>    </div>  );}
    client a 01
    connected
    state.counter 0
    websocket
    this.setState
    broadcast
    client b 02
    connected
    state.counter 0
    acto v composición

    Composición.

    Un agente llama a otros agentes. 4 servicios pegados con webhooks se vuelven un DO + bindings. La orquestación deja de vivir en glue code y vive en el lenguaje del programa.

    orchestration topology
    cycles: 0
    Concierge orchestrator Research agent Writer agent Reviewer agent Publisher agent
    concierge.ts
    orchestrator
    export class Concierge extends Agent {  async onRequest(topic) {    const r = await this.callAgent(      'Research', { topic });    const draft = await this.callAgent(      'Writer', { sources: r });    const ok = await this.callAgent(      'Reviewer', { draft });    if (ok) await this.callAgent(      'Publisher', { draft });  }}
    live messages
      CODA · agents-sdk · tier 6

      lo que era plumbing
      ahora es lógica.

      Eigen Atlas construye sobre este stack. Cada cliente vive en su propio DO, cada decisión que pinta en el negocio reside en código que tu equipo puede leer en una sentada.

      líneas reducidas
      127 29
      ~77% menos plumbing
      primitivas integradas
      7
      DO · SQLite · WS · scheduler · MCP · React · multi-agent
      en producción
      CF
      workers paid · $5/mo
      eigenatlas builds these