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.
Incluida en el Pase · para Cloudflare Workers, Cloudflare Durable Objects, React
/**
- 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.sqlINSERT 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.sql
SELECT * 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
// opiniones_de_la_comunidad
Opiniones
Cargando opiniones…