import { useAgent } from "agents/react"; import { useState, useCallback } from "react"; import { Button, Input, Badge, Text } from "@cloudflare/kumo"; import { ConnectionIndicator, ModeToggle, PoweredByAgents } from "@cloudflare/agents-ui"; import type { ConnectionStatus } from "@cloudflare/agents-ui"; import type { ResearchAgent, ProgressMessage, ResearchStep, AgentState } from "./server"; type StepStatus = "pending" | "running" | "complete" | "skipped"; type StepInfo = { name: string; status: StepStatus; result?: string; }; export default function App() { const [topic, setTopic] = useState("quantum computing"); const [steps, setSteps] = useState([]); const [fiberId, setFiberId] = useState(null); const [status, setStatus] = useState< "idle" | "running" | "complete" | "cancelled" | "recovered" >("idle"); const [results, setResults] = useState([]); const [connectionStatus, setConnectionStatus] = useState("connecting"); const agent = useAgent({ agent: "research-agent", onMessage: (message) => { try { const raw = typeof message === "string" ? message : message.data; const msg = JSON.parse(raw as string) as ProgressMessage; switch (msg.type) { case "research:started": setFiberId(msg.fiberId); setStatus("running"); setResults([]); setSteps( msg.steps.map((name, i) => ({ name, status: i === 0 ? "running" : "pending" })) ); break; case "research:step": setSteps((prev) => prev.map((s, i) => { if (i === msg.stepIndex) return { ...s, status: "complete", result: msg.result }; if (i === msg.stepIndex + 1) return { ...s, status: "running" }; return s; }) ); break; case "research:recovered": setStatus("recovered"); setSteps((prev) => prev.map((s, i) => { if (i < msg.skippedSteps) return { ...s, status: "skipped" }; if (i === msg.skippedSteps) return { ...s, status: "running" }; return { ...s, status: "pending" }; }) ); setTimeout(() => setStatus("running"), 1500); break; case "research:complete": setStatus("complete"); setResults(msg.results); break; case "research:cancelled": setStatus("cancelled"); break; case "research:failed": setStatus("idle"); break; } } catch { // Non-JSON messages (state sync, etc.) } }, onOpen: () => setConnectionStatus("connected"), onClose: () => setConnectionStatus("disconnected") }); const handleStart = useCallback(async () => { if (!topic.trim() || !agent) return; await agent.call("startResearch", [topic.trim()]); }, [topic, agent]); const handleCancel = useCallback(async () => { if (!agent) return; await agent.call("cancelResearch", []); }, [agent]); const handleKillAndRecover = useCallback(async () => { if (!agent) return; setStatus("recovered"); await agent.call("simulateKillAndRecover", []); // Recovery is async — the fiber restarts in the background // and broadcasts progress updates as it resumes setTimeout(() => { if (status === "recovered") setStatus("running"); }, 2000); }, [agent, status]); const isRunning = status === "running" || status === "recovered"; return (
{/* Header */}
Long-Running Agent Fibers
{/* Main */}
{/* Input */}
Research Topic
setTopic(e.target.value)} placeholder="Enter a research topic..." disabled={isRunning} onKeyDown={(e) => { if (e.key === "Enter") handleStart(); }} /> {isRunning ? ( ) : ( )}
{/* Steps */} {steps.length > 0 && (
Progress {fiberId && ( {fiberId.slice(0, 8)}... )}
{steps.map((step, i) => (
{step.name} {step.result && ( {step.result} )} {step.status === "skipped" && ( Restored from checkpoint )}
))}
)} {/* Eviction Demo */} {isRunning && (
Simulate Eviction

In production, Durable Objects can be evicted by code updates or inactivity. This simulates that — the fiber state persists in SQLite, and recovery picks up from the last checkpoint.

)} {/* Recovery banner */} {status === "recovered" && (
Fiber recovered from checkpoint. Skipping already-completed steps...
)} {/* Completion */} {status === "complete" && results.length > 0 && (
Research Complete
{results.map((r, i) => (
{r.name} {r.result}
))}
)} {/* Cancelled */} {status === "cancelled" && (
Research cancelled.
)}
{/* Footer */}
); } function StepIcon({ status }: { status: StepStatus }) { switch (status) { case "complete": case "skipped": return ; case "running": return ; default: return ; } } function StepBadge({ status }: { status: StepStatus }) { const variant: React.ComponentProps["variant"] = status === "complete" ? "outline" : status === "running" ? "primary" : status === "skipped" ? "beta" : "secondary"; return {status}; }