← Volver al catálogo
Agentes IAReferenciaAvanzadoEn el pase

Cloudflare Agents SDK — Referencia completa

Guía de referencia técnica completa para construir agentes IA stateful en Cloudflare Workers usando el Agents SDK: estado persistente, workflows duraderos, servidores MCP, chat con streaming, tareas programadas y automatización de navegador.

Comprobando acceso…

Incluida en el Pase · para Cloudflare Workers, Cloudflare Durable Objects, React

// resultado_de_ejemplo

/**

  • NutriFlow — Lead Qualification Agent
  • =====================================
  • Agente IA stateful para cualificación automática de leads B2B.
  • Construido con Cloudflare Agents SDK sobre Durable Objects + SQLite.
  • Capacidades utilizadas del SDK:
  • ✓ Estado persistente (setState / SQL)
  • ✓ Chat streaming (AIChatAgent)
  • ✓ Scheduling / follow-ups (schedule / scheduleEvery)
  • ✓ RPC callable (@callable)
  • ✓ MCP server (McpAgent)
  • ✓ Webhooks de entrada (onRequest)
  • ✓ Observabilidad (diagnostics_channel)
  • ✓ React client (useAgentChat)
  • Instalación:
  • npm install agents @cloudflare/ai-chat ai @ai-sdk/react
  • wrangler.jsonc mínimo:
  • {
  • "compatibility_flags": ["nodejs_compat"],
    
  • "durable_objects": {
    
  •   "bindings": [
    
  •     { "name": "LeadAgent",    "class_name": "LeadQualificationAgent" },
    
  •     { "name": "LeadsMcpHub", "class_name": "NutriFlowMcpAgent" }
    
  •   ]
    
  • },
    
  • "migrations": [
    
  •   { "tag": "v1", "new_sqlite_classes": ["LeadQualificationAgent", "NutriFlowMcpAgent"] }
    
  • ],
    
  • "ai": { "binding": "AI" }
    
  • } */

import { Agent, AIChatAgent, McpAgent, routeAgentRequest, callable, type Connection, } from "agents"; import { createWorkersAI } from "@cloudflare/ai-chat"; import { tool } from "ai"; import { z } from "zod"; import * as diagnostics_channel from "node:diagnostics_channel";

// ───────────────────────────────────────────── // 1. TIPOS Y ESTADO // ─────────────────────────────────────────────

/** Fases de cualificación BANT */ type LeadStage = | "nuevo" | "en_cualificacion" | "cualificado" | "no_cualificado" | "seguimiento_programado";

interface BantAnswers { budget?: string; // Budget: ¿tiene presupuesto? authority?: string; // Authority: ¿es el decisor? need?: string; // Need: ¿qué problema resuelve? timeline?: string; // Timeline: ¿cuándo necesita solución? }

/** Estado persistente del agente — serializado en SQLite (DO) */ interface LeadState { leadId: string; nombre: string; email: string; empresa: string; empleados?: number; stage: LeadStage; bant: BantAnswers; score: number; // 0-100 followUpScheduledAt?: string; slackNotified: boolean; createdAt: string; updatedAt: string; }

// ───────────────────────────────────────────── // 2. AGENTE PRINCIPAL — LeadQualificationAgent // ─────────────────────────────────────────────

export class LeadQualificationAgent extends AIChatAgent<Env, LeadState> { /**

  • Estado inicial cuando se crea la instancia por primera vez.
  • Persistido en SQLite del Durable Object. */ initialState: LeadState = { leadId: "", nombre: "", email: "", empresa: "", stage: "nuevo", bant: {}, score: 0, slackNotified: false, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), };

/**

  • Validación de cambios de estado — previene datos inválidos.
  • Se ejecuta antes de persistir cualquier actualización. */ validateStateChange( next: LeadState, _source: Connection | "server" ): void { if (next.score < 0 || next.score > 100) { throw new Error("Score debe estar entre 0 y 100"); } if (!next.email.includes("@")) { throw new Error("Email inválido"); } }

/**

  • Hook que se ejecuta tras cada actualización de estado.
  • Aquí disparamos la notificación a Slack si el lead está cualificado. */ async onStateUpdate( state: LeadState, _source: Connection | "server" ): Promise { if (state.stage === "cualificado" && !state.slackNotified) { await this.notifySlack(state); this.setState({ ...state, slackNotified: true }); } }

// ── 2a. Entrada de webhook (nuevo lead desde formulario web) ──

/**

  • onRequest: recibe el webhook POST /agents/lead-qualification-agent/{leadId}

  • con los datos iniciales del formulario.

  • Inicializa el estado y arranca el chat de cualificación. */ async onRequest(request: Request): Promise<Response | void> { if (request.method === "POST") { const body = await request.json<{ nombre: string; email: string; empresa: string; empleados?: number; }>();

    // Inicializamos el estado con los datos del formulario this.setState({ ...this.state, leadId: this.name, // El nombre de la instancia ES el leadId nombre: body.nombre, email: body.email, empresa: body.empresa, empleados: body.empleados, stage: "en_cualificacion", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), });

    // Guardamos también en SQL para queries analíticas await this.sql INSERT OR REPLACE INTO leads (id, email, empresa, stage, score, created_at) VALUES ( ${this.name}, ${body.email}, ${body.empresa}, 'en_cualificacion', 0, ${new Date().toISOString()} ) ;

    // Programamos follow-up automático en 24 horas si no hay respuesta await this.scheduleFollowUp();

    return new Response( JSON.stringify({ ok: true, agentId: this.name, stage: "en_cualificacion" }), { status: 200, headers: { "Content-Type": "application/json" } } ); } // Para WebSocket/GET, el SDK maneja automáticamente la sesión de chat }

// ── 2b. Sistema de IA para el chat de cualificación ──

/**

  • getSystemPrompt: configura el comportamiento del chat IA.
  • AIChatAgent lo usa para cada turno de conversación. */ getSystemPrompt(): string { const lead = this.state; return ` Eres el asistente de cualificación de ventas de NutriFlow, un SaaS de gestión nutricional para clínicas. Estás hablando con ${lead.nombre} de ${lead.empresa}.

Tu objetivo es completar la cualificación BANT de forma natural y conversacional:

  • Budget: ¿tienen presupuesto para herramientas de gestión nutricional?
  • Authority: ¿${lead.nombre} es quien toma decisiones de compra de software?
  • Need: ¿qué problema concreto quieren resolver?
  • Timeline: ¿en cuánto tiempo buscan implementar una solución?

Información ya recopilada: ${JSON.stringify(lead.bant, null, 2)}

Cuando tengas las 4 respuestas BANT, usa la herramienta "qualifyLead" para actualizar el score y la etapa. Sé amable, profesional y conciso. No hagas más de una pregunta a la vez. Habla en español. `.trim(); }

/**

  • getAIModel: define el modelo LLM a usar.
  • Aquí usamos Workers AI (llama-3.1-8b-instruct) — sin coste extra en CF. */ getAIModel() { return createWorkersAI({ binding: (this.env as any).AI }); }

/**

  • getTools: herramientas disponibles para el modelo durante el chat.

  • Le permiten al LLM actualizar el estado del lead. */ getTools() { return { // Herramienta para actualizar respuestas BANT y recalcular score qualifyLead: tool({ description: "Actualiza la cualificación BANT del lead y calcula el score", parameters: z.object({ budget: z.string().optional().describe("Respuesta sobre presupuesto"), authority: z.string().optional().describe("Respuesta sobre autoridad de decisión"), need: z.string().optional().describe("Necesidad principal del cliente"), timeline: z.string().optional().describe("Plazo para implementar la solución"), }), execute: async (params) => { const bant = { ...this.state.bant, ...params }; const score = this.calculateBantScore(bant); const stage: LeadStage = score >= 70 ? "cualificado" : score >= 40 ? "en_cualificacion" : "no_cualificado"; this.setState({ ...this.state, bant, score, stage, updatedAt: new Date().toISOString(), }); // Cancelamos el follow-up programado si ya está cualificado if (stage === "cualificado") { await this.cancelPendingFollowUps(); } return { score, stage, bant }; }, }),

    // Herramienta para agendar demo directamente desde el chat scheduleDemoCall: tool({ description: "Agenda una llamada de demo con el equipo de ventas", parameters: z.object({ fecha: z.string().describe("Fecha preferida (ISO 8601)"), duracion: z.number().default(30).describe("Duración en minutos"), }), execute: async ({ fecha, duracion }) => { // En producción: integrar con Calendly/Google Calendar API const demoId = demo-${this.name}-${Date.now()}; await this.sql INSERT INTO demos (id, lead_id, scheduled_at, duration_minutes) VALUES (${demoId}, ${this.name}, ${fecha}, ${duracion}) ; return { demoId, confirmado: true, fecha, duracion }; }, }), }; }

// ── 2c. Scheduling — follow-ups automáticos ──

/**

  • schedule(): programa una tarea diferida.
  • Si el lead no responde en 24 h, enviamos un email de seguimiento. */ async scheduleFollowUp(): Promise { const twentyFourHours = 24 * 60 * 60; // segundos
// schedule(delay_en_segundos, nombre_del_metodo, payload)
const task = await this.schedule(twentyFourHours, "sendFollowUpEmail", {
  leadId: this.name,
  attempt: 1,
});

this.setState({
  ...this.state,
  stage: "seguimiento_programado",
  followUpScheduledAt: new Date(Date.now() + twentyFourHours * 1000).toISOString(),
});

console.log(`[LeadAgent] Follow-up programado: ${task.id} para ${this.state.email}`);

}

/**

  • sendFollowUpEmail: método invocado por el scheduler.
  • El SDK lo llama automáticamente cuando llega el momento. */ async sendFollowUpEmail(payload: { leadId: string; attempt: number }): Promise { if (this.state.stage === "cualificado") { console.log([LeadAgent] Lead ya cualificado, cancelando follow-up); return; }
// En producción: usar Cloudflare Email Routing o SendGrid
console.log(`[LeadAgent] Enviando follow-up email #${payload.attempt} a ${this.state.email}`);

// Si es el primer intento, programamos uno más a los 3 días
if (payload.attempt < 3) {
  await this.schedule(3 * 24 * 60 * 60, "sendFollowUpEmail", {
    leadId: payload.leadId,
    attempt: payload.attempt + 1,
  });
}

}

/**

  • scheduleEvery(): tareas recurrentes.
  • Ejemplo: reporte diario de leads activos al equipo. */ async startDailyReport(): Promise { // scheduleEvery(intervalo_en_segundos, nombre_del_metodo) await this.scheduleEvery(24 * 60 * 60, "generateDailyReport"); }

async generateDailyReport(): Promise { const rows = await this.sqlSELECT stage, COUNT(*) as total FROM leads GROUP BY stage; console.log("[LeadAgent] Reporte diario:", rows); // En producción: enviar a Slack / dashboard }

// ── 2d. Callable RPC — métodos invocables desde el cliente ──

/**

  • @callable(): expone este método como RPC sobre WebSocket.
  • El cliente React puede llamarlo directamente con agent.call("getLeadStatus"). */ @callable() getLeadStatus(): { stage: LeadStage; score: number; bant: BantAnswers } { return { stage: this.state.stage, score: this.state.score, bant: this.state.bant, }; }

/**

  • @callable con streaming: ideal para operaciones lentas.
  • Devuelve los leads de la BD progresivamente. */ @callable({ streaming: true }) async *streamLeadHistory(res: AsyncGenerator) { const rows = await this.sqlSELECT * FROM leads ORDER BY created_at DESC LIMIT 50; for (const row of rows) { yield JSON.stringify(row) + "\n"; } }

@callable() async resetLead(): Promise { this.setState({ ...this.initialState, leadId: this.name }); }

// ── 2e. Utilidades privadas ──

private calculateBantScore(bant: BantAnswers): number { let score = 0; if (bant.budget) score += 25; if (bant.authority) score += 25; if (bant.need) score += 30; if (bant.timeline) score += 20; // Bonus si el timeline es corto (< 3 meses) if (bant.timeline?.toLowerCase().includes("mes")) score += 10; return Math.min(score, 100); }

private async notifySlack(lead: LeadState): Promise { const webhookUrl = (this.env as any).SLACK_WEBHOOK_URL; if (!webhookUrl) return;

await fetch(webhookUrl, {
  method: "POST",
  body: JSON.stringify({
    text: `🎯 *Nuevo lead cualificado en NutriFlow*`,
    blocks: [
      {
        type: "section",
        text: {
          type: "mrkdwn",
          text: `*${lead.nombre}* de *${lead.empresa}*\nScore: ${lead.score}/100\nEmail: ${lead.email}`,
        },
      },
      {
        type: "section",
        fields: [
          { type: "mrkdwn", text: `*Budget:*\n${lead.bant.budget ?? "—"}` },
          { type: "mrkdwn", text: `*Timeline:*\n${lead.bant.timeline ?? "—"}` },
        ],
      },
    ],
  }),
  headers: { "Content-Type": "application/json" },
});

}

private async cancelPendingFollowUps(): Promise { // Obtenemos las tareas programadas y cancelamos las de follow-up const tasks = await this.getSchedule(); for (const task of tasks) { if (task.type === "sendFollowUpEmail") { await task.cancel(); } } } }

// ───────────────────────────────────────────── // 3. MCP SERVER — NutriFlowMcpAgent // ─────────────────────────────────────────────

/**

  • McpAgent: expone herramientas MCP para que otros agentes
  • (ej. agente de reporting, agente de onboarding) consulten leads.
  • URL de conexión: https://nutriflow.workers.dev/mcp
  • Transport: Streamable HTTP (recomendado para MCP en Workers) */ export class NutriFlowMcpAgent extends McpAgent<Env, {}, {}> { server = this.initializeMcpServer({ name: "nutriflow-leads", version: "1.0.0", });

async init() { // Herramienta MCP 1: consultar estado de un lead this.server.tool( "get_lead_status", "Obtiene el estado de cualificación de un lead por su ID", { leadId: z.string() }, async ({ leadId }) => { const agent = await (this.env as any).LeadAgent.get( (this.env as any).LeadAgent.idFromName(leadId) ); const status = await agent.call("getLeadStatus"); return { content: [{ type: "text", text: JSON.stringify(status, null, 2) }], }; } );

// Herramienta MCP 2: listar leads cualificados (SQL directo)
this.server.tool(
  "list_qualified_leads",
  "Lista todos los leads con score >= 70 listos para ventas",
  { limit: z.number().default(10) },
  async ({ limit }) => {
    // En producción: query a D1 o el DO de analítica
    const mockLeads = [
      { id: "lead-001", empresa: "Clínica NutriSalud", score: 85, stage: "cualificado" },
      { id: "lead-002", empresa: "Centro Dietético Wellbeing", score: 92, stage: "cualificado" },
    ].slice(0, limit);

    return {
      content: [
        {
          type: "text",
          text: `Leads cualificados (${mockLeads.length}):\n${JSON.stringify(mockLeads, null, 2)}`,
        },
      ],
    };
  }
);

// Herramienta MCP 3: actualizar etapa manualmente (para agente de ventas)
this.server.tool(
  "update_lead_stage",
  "Actualiza la etapa de un lead (uso exclusivo del agente de ventas)",
  {
    leadId: z.string(),
    stage: z.enum(["cualificado", "no_cualificado", "en_cualificacion"]),
    nota: z.string().optional(),
  },
  async ({ leadId, stage, nota }) => {
    // En producción: actualizar el estado del DO correspondiente
    return {
      content: [
        {
          type: "text",
          text: `Lead ${leadId} actualizado a etapa: ${stage}${nota ? `. Nota: ${nota}` : ""}`,
        },
      ],
    };
  }
);

} }

// ───────────────────────────────────────────── // 4. OBSERVABILIDAD // ─────────────────────────────────────────────

/**

  • diagnostics_channel: suscripción a eventos internos del SDK.
  • Útil para logging estructurado, métricas y alertas. */ diagnostics_channel.subscribe("agents:state", (data: any) => { console.log("[Observability] Estado actualizado:", { agentId: data.agent?.name, stage: data.state?.stage, score: data.state?.score, ts: new Date().toISOString(), }); });

diagnostics_channel.subscribe("agents:schedule", (data: any) => { console.log("[Observability] Tarea programada:", { agentId: data.agent?.name, taskType: data.task?.type, scheduledAt: data.task?.scheduledAt, }); });

// ───────────────────────────────────────────── // 5. WORKER ENTRYPOINT // ─────────────────────────────────────────────

export default { /**

  • routeAgentRequest: enruta automáticamente a la clase correcta
  • según el patrón de URL:
  • /agents/lead-qualification-agent/{leadId} → LeadQualificationAgent
  • /mcp → NutriFlowMcpAgent
  • /agents/nutri-flow-mcp-agent/{id} → NutriFlowMcpAgent */ async fetch(request: Request, env: Env): Promise { const url = new URL(request.url);
// MCP endpoint dedicado (Streamable HTTP)
if (url.pathname === "/mcp") {
  // El McpAgent maneja la negociación de transporte automáticamente
  return NutriFlowMcpAgent.serve("/mcp").fetch(request, env);
}

// Routing estándar del Agents SDK para el resto de rutas
return (
  routeAgentRequest(request, env) ??
  new Response("NutriFlow Agents API — ruta no encontrada", { status: 404 })
);

}, };

// ───────────────────────────────────────────── // 6. CLIENTE REACT — Widget de Chat // ───────────────────────────────────────────── // Archivo separado: src/components/LeadChatWidget.tsx

/* import { useAgentChat } from "agents/react"; import { useEffect, useState } from "react";

export function LeadChatWidget({ leadId }: { leadId: string }) { const { messages, input, handleInputChange, handleSubmit, status } = useAgentChat({ agent: "lead-qualification-agent", name: leadId, });

return (

{messages.map((m) => ( <div key={m.id} className={msg msg--${m.role}}> {m.content} ))} {status === "streaming" && ...} Enviar ); } */

// ───────────────────────────────────────────── // 7. TIPOS DE ENTORNO (Env) // ─────────────────────────────────────────────

interface Env { // Bindings de Durable Objects (definidos en wrangler.jsonc) LeadAgent: DurableObjectNamespace; LeadsMcpHub: DurableObjectNamespace;

// Workers AI binding AI: Ai;

// Secrets (definidos en wrangler secret put) SLACK_WEBHOOK_URL: string; OPENAI_API_KEY?: string; // Alternativa a Workers AI }

// qué_hace

Proporciona una referencia estructurada con código TypeScript listo para usar sobre todas las capacidades del Agents SDK de Cloudflare: estado, RPC, scheduling, workflows, MCP, chat, voz y automatización web.

// cómo_lo_hace

Organiza la documentación oficial en tablas de referencia rápida y snippets de código TypeScript/React comentados, con enlaces directos a los docs de Cloudflare para cada capacidad del SDK.

// ejemplo_de_uso

Consulta este recurso cuando quieras construir un agente con estado y scheduling que corra en Cloudflare Workers sin gestionar infraestructura. Ej.: un desarrollador monta un agente de soporte con memoria persistente y cola de tareas usando los snippets de Durable Objects del SDK.

// plataformas

Cloudflare WorkersCloudflare Durable ObjectsReactNode.js
Categoría
Agentes IA
Tipo
Referencia
Nivel
Avanzado
Licencia
Apache-2.0
Seguridad
seguro · riesgo bajo
Versión
1.0.0

// opiniones_de_la_comunidad

Opiniones

Cargando opiniones…