branch:
SupervisorDemo.tsx
9564 bytesRaw
import { useAgent } from "agents/react";
import { nanoid } from "nanoid";
import { useState, useEffect, useCallback } from "react";
import { Button, Surface, Empty, Text } from "@cloudflare/kumo";
import { DemoWrapper } from "../../layout";
import {
LogPanel,
ConnectionStatus,
CodeExplanation,
type CodeSection
} from "../../components";
import { useLogs, useUserId } from "../../hooks";
import type { ChildState } from "./child-agent";
import type { SupervisorAgent, SupervisorState } from "./supervisor-agent";
interface ChildInfo {
id: string;
state: ChildState;
}
const codeSections: CodeSection[] = [
{
title: "Spawn child agents with getAgentByName",
description:
"The supervisor creates child agents by calling getAgentByName(). Each child is a separate Durable Object with its own state and lifecycle.",
code: `import { Agent, callable, getAgentByName } from "agents";
class SupervisorAgent extends Agent<Env> {
@callable()
async createChild(childId: string) {
const child = await getAgentByName(this.env.ChildAgent, childId);
await child.initialize({ createdBy: this.name });
return { id: childId, status: "created" };
}
}`
},
{
title: "Coordinate across children",
description:
"The supervisor can call methods on any child via Durable Object RPC. Fan out to all children with Promise.all() for parallel operations.",
code: ` @callable()
async incrementAll() {
const results = await Promise.all(
this.state.childIds.map(async (id) => {
const child = await getAgentByName(this.env.ChildAgent, id);
return child.increment();
})
);
return { updated: results.length };
}`
}
];
export function SupervisorDemo() {
const userId = useUserId();
const { logs, addLog, clearLogs } = useLogs();
const [children, setChildren] = useState<ChildInfo[]>([]);
const [stats, setStats] = useState({ totalChildren: 0, totalCounter: 0 });
const agent = useAgent<SupervisorAgent, SupervisorState>({
agent: "supervisor-agent",
name: `demo-supervisor-${userId}`,
onOpen: () => {
addLog("info", "connected");
refreshStats();
},
onClose: () => addLog("info", "disconnected"),
onError: () => addLog("error", "error", "Connection error")
});
const refreshStats = useCallback(async () => {
try {
const result = await agent.call("getStats");
setChildren(result.children);
setStats({
totalChildren: result.totalChildren,
totalCounter: result.totalCounter
});
addLog("in", "stats", result);
} catch (e) {
addLog("error", "error", e instanceof Error ? e.message : String(e));
}
}, [agent, addLog]);
const handleCreateChild = async () => {
const childId = `child-${nanoid(6)}`;
addLog("out", "call", `createChild("${childId}")`);
try {
const result = await agent.call("createChild", [childId]);
addLog("in", "result", result);
await refreshStats();
} catch (e) {
addLog("error", "error", e instanceof Error ? e.message : String(e));
}
};
const handleIncrementChild = async (childId: string) => {
addLog("out", "call", `incrementChild("${childId}")`);
try {
const result = await agent.call("incrementChild", [childId]);
addLog("in", "result", result);
await refreshStats();
} catch (e) {
addLog("error", "error", e instanceof Error ? e.message : String(e));
}
};
const handleIncrementAll = async () => {
addLog("out", "call", "incrementAll()");
try {
const result = await agent.call("incrementAll");
addLog("in", "result", result);
await refreshStats();
} catch (e) {
addLog("error", "error", e instanceof Error ? e.message : String(e));
}
};
const handleRemoveChild = async (childId: string) => {
addLog("out", "call", `removeChild("${childId}")`);
try {
await agent.call("removeChild", [childId]);
await refreshStats();
} catch (e) {
addLog("error", "error", e instanceof Error ? e.message : String(e));
}
};
const handleClearAll = async () => {
addLog("out", "call", "clearChildren()");
try {
await agent.call("clearChildren");
setChildren([]);
setStats({ totalChildren: 0, totalCounter: 0 });
} catch (e) {
addLog("error", "error", e instanceof Error ? e.message : String(e));
}
};
useEffect(() => {
if (agent.readyState === WebSocket.OPEN) {
refreshStats();
}
}, [agent.readyState, refreshStats]);
return (
<DemoWrapper
title="Supervisor Pattern"
description={
<>
A supervisor agent creates and manages child agents using{" "}
<code className="text-xs bg-kumo-fill px-1 py-0.5 rounded">
getAgentByName()
</code>
. Each child is a separate Durable Object with its own state and
lifecycle. The supervisor coordinates them via Durable Object RPC —
calling methods, aggregating results, and tracking their state. Create
a few children below and increment them individually or all at once.
</>
}
statusIndicator={
<ConnectionStatus
status={
agent.readyState === WebSocket.OPEN ? "connected" : "connecting"
}
/>
}
>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Controls */}
<div className="space-y-6">
{/* Connection & Stats */}
<Surface className="p-4 rounded-lg ring ring-kumo-line">
{/* Stats Bar */}
<div className="flex gap-4 text-sm mb-4">
<div className="flex-1 bg-kumo-control rounded p-3 text-center">
<div className="text-2xl font-bold text-kumo-default">
{stats.totalChildren}
</div>
<div className="text-kumo-subtle text-xs">Children</div>
</div>
<div className="flex-1 bg-kumo-control rounded p-3 text-center">
<div className="text-2xl font-bold text-kumo-default">
{stats.totalCounter}
</div>
<div className="text-kumo-subtle text-xs">Total Counter</div>
</div>
</div>
{/* Actions */}
<div className="flex gap-2">
<Button variant="primary" onClick={handleCreateChild}>
+ Create Child
</Button>
<Button
variant="secondary"
onClick={handleIncrementAll}
disabled={children.length === 0}
>
+1 to All
</Button>
</div>
</Surface>
{/* Children Grid */}
<Surface className="p-4 rounded-lg ring ring-kumo-line">
<div className="flex items-center justify-between mb-4">
<Text variant="heading3">Child Agents ({children.length})</Text>
{children.length > 0 && (
<Button
variant="ghost"
size="xs"
onClick={handleClearAll}
className="text-kumo-danger"
>
Clear All
</Button>
)}
</div>
{children.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{children.map((child) => (
<div
key={child.id}
className="border border-kumo-line rounded p-3"
>
<div className="flex items-center justify-between mb-2">
<code className="text-xs text-kumo-subtle">
{child.id}
</code>
<Button
variant="ghost"
shape="square"
size="xs"
aria-label="Remove child agent"
onClick={() => handleRemoveChild(child.id)}
className="text-kumo-danger"
>
×
</Button>
</div>
<div className="flex items-center justify-between">
<span className="text-2xl font-bold text-kumo-default">
{child.state.counter}
</span>
<Button
variant="secondary"
size="xs"
onClick={() => handleIncrementChild(child.id)}
>
+1
</Button>
</div>
{child.state.createdAt && (
<div className="text-xs text-kumo-inactive mt-2">
{new Date(child.state.createdAt).toLocaleTimeString()}
</div>
)}
</div>
))}
</div>
) : (
<Empty
title='No children yet. Click "Create Child" to spawn a new child agent.'
size="sm"
/>
)}
</Surface>
<CodeExplanation sections={codeSections} />
</div>
{/* Logs */}
<div className="space-y-6">
<LogPanel logs={logs} onClear={clearLogs} maxHeight="600px" />
</div>
</div>
</DemoWrapper>
);
}