Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions docs/callable-methods.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,22 @@ console.log(result); // "Hello, World!"

The `@callable()` decorator is specifically for WebSocket-based RPC from external clients. When calling from within the same Worker or another agent, use standard [Durable Object RPC](https://developers.cloudflare.com/durable-objects/best-practices/create-durable-object-stubs-and-send-requests/) directly.

## TypeScript Configuration

The `@callable()` decorator requires TypeScript's decorator support. Set `"target"` to `"ES2021"` or later in your `tsconfig.json`:

```json
{
"compilerOptions": {
"target": "ES2021"
}
}
```

Without this, your dev server will fail with `SyntaxError: Invalid or unexpected token`. Setting the target to `ES2021` ensures that Vite's esbuild transpiler downlevels TC39 decorators instead of passing them through as native syntax.

> **Warning:** Do not set `"experimentalDecorators": true` in your `tsconfig.json`. The Agents SDK uses [TC39 standard decorators](https://github.com/tc39/proposal-decorators), not TypeScript legacy decorators. Enabling `experimentalDecorators` applies an incompatible transform that silently breaks `@callable()` at runtime.

## Basic Usage

### Defining Callable Methods
Expand Down
2 changes: 1 addition & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ For Workers AI integration:
Access in your agent:

```typescript
const response = await this.env.AI.run("@cf/meta/llama-3-8b-instruct", {
const response = await this.env.AI.run("@cf/zai-org/glm-4.7-flash", {
prompt: "Hello!"
});
```
Expand Down
23 changes: 11 additions & 12 deletions examples/playground/TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,30 @@

## Make Docs-Only Demos Interactive

- [ ] **Live AI Chat** - Integrate actual OpenAI/Workers AI for a working chat demo with streaming responses and tool calls
- [ ] **Working MCP Server** - The playground itself could expose an MCP server that external clients (Cursor, Claude) can connect to
- [ ] **MCP Client Demo** - Connect to the playground's own MCP server to demonstrate the client flow
- [x] **Live AI Chat** - Working chat with Workers AI (glm-4.7-flash), streaming, and client-side tools
- [x] **MCP Server** - Real MCP server at /mcp-server with tools (roll_dice, generate_uuid, word_count, hash_text) and resources
- [x] **MCP Client** - Connects to the playground's own MCP server, discovers tools via onMcpUpdate, calls them
- [x] **Workflow Demos** - Interactive multi-step workflow simulation and approval patterns
- [x] **Email Demos** - Real email receiving and secure replies via Cloudflare Email Routing
- [x] **Workers Pattern** - Interactive fan-out demo with ManagerAgent distributing to parallel FanoutWorkerAgents
- [x] **Pipeline Pattern** - Interactive 3-stage pipeline (Validate → Transform → Enrich) with per-stage output

## Missing SDK Features

- [ ] **Hibernation** - Demo showing hibernatable WebSockets and cost savings patterns
- [x] **Multi-Agent** - One agent calling another agent (agent-to-agent communication)
- [ ] **HTTP API** - Show `getAgentByName()` for HTTP-only access without WebSockets
- [ ] **Queue Patterns** - Rate limiting, batching, deduplication using the queue
- [x] **Multi-Agent** - One agent calling another agent (agent-to-agent communication)
- [x] **Routing Strategies** - Different agent naming patterns (per-user, per-session, shared)

## Developer Experience

- [ ] **Code Examples** - Bring back server/client code snippets in a better way (e.g., link to actual source files, modal view, or separate "Code" tab per demo)
- [ ] **Network Inspector** - Raw WebSocket frame viewer showing the actual protocol messages
- [ ] **Agent Inspector** - View internal tables (cf_agents_state, cf_agents_schedules, etc.)
- [ ] **State Diff View** - Highlight what changed in state updates
- [ ] **Copy-Paste Templates** - One-click starter code for each feature

## Polish

- [x] **Dark Mode Toggle** - Light/dark toggle fitting the grayscale theme
- [ ] **Mobile Sidebar** - Collapsible hamburger menu for mobile
- [ ] **Keyboard Shortcuts** - Navigate demos with arrow keys
- [ ] **Progress Indicator** - Show which demos the user has explored
- [x] **Code Examples** - "How it works" sections on every demo page with Shiki syntax-highlighted literate code snippets
- [x] **Rich Descriptions** - Fleshed-out descriptions with inline code tags on every demo page
- [x] **Back Navigation** - Back button on every demo page linking to the home page
- [x] **JSON Highlighting** - All JSON output uses Shiki syntax highlighting instead of plain CodeBlock
- [x] **Idle Cleanup** - All agents self-destroy after 15 minutes without connections (PlaygroundAgent base class)
32 changes: 31 additions & 1 deletion examples/playground/env.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable */
// Generated by Wrangler by running `wrangler types env.d.ts --include-runtime false` (hash: 520f72d978a6af1f4217b0902208b889)
// Generated by Wrangler by running `wrangler types env.d.ts --include-runtime false` (hash: e23c437483faad42c0a968a88ae8dab5)
declare namespace Cloudflare {
interface GlobalProps {
mainModule: typeof import("./src/server");
Expand All @@ -20,6 +20,14 @@ declare namespace Cloudflare {
| "ChildAgent"
| "LobbyAgent"
| "RoomAgent"
| "ManagerAgent"
| "FanoutWorkerAgent"
| "PipelineOrchestratorAgent"
| "ValidatorStageAgent"
| "TransformStageAgent"
| "EnrichStageAgent"
| "PlaygroundMcpServer"
| "McpClientAgent"
| "BasicWorkflowAgent"
| "ApprovalAgent"
| "ReceiveEmailAgent"
Expand Down Expand Up @@ -52,6 +60,28 @@ declare namespace Cloudflare {
ChildAgent: DurableObjectNamespace<import("./src/server").ChildAgent>;
LobbyAgent: DurableObjectNamespace<import("./src/server").LobbyAgent>;
RoomAgent: DurableObjectNamespace<import("./src/server").RoomAgent>;
ManagerAgent: DurableObjectNamespace<import("./src/server").ManagerAgent>;
FanoutWorkerAgent: DurableObjectNamespace<
import("./src/server").FanoutWorkerAgent
>;
PipelineOrchestratorAgent: DurableObjectNamespace<
import("./src/server").PipelineOrchestratorAgent
>;
ValidatorStageAgent: DurableObjectNamespace<
import("./src/server").ValidatorStageAgent
>;
TransformStageAgent: DurableObjectNamespace<
import("./src/server").TransformStageAgent
>;
EnrichStageAgent: DurableObjectNamespace<
import("./src/server").EnrichStageAgent
>;
PlaygroundMcpServer: DurableObjectNamespace<
import("./src/server").PlaygroundMcpServer
>;
McpClientAgent: DurableObjectNamespace<
import("./src/server").McpClientAgent
>;
BasicWorkflowAgent: DurableObjectNamespace<
import("./src/server").BasicWorkflowAgent
>;
Expand Down
1 change: 1 addition & 0 deletions examples/playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"nanoid": "^5.1.6",
"postal-mime": "^2.7.3",
"react-router-dom": "^7.13.0",
"shiki": "^3.22.0",
"streamdown": "^2.2.0"
},
"devDependencies": {
Expand Down
97 changes: 97 additions & 0 deletions examples/playground/src/components/CodeExplanation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { useEffect, useState } from "react";
import { Surface, Text } from "@cloudflare/kumo";
import { CodeIcon } from "@phosphor-icons/react";
import { createHighlighter, type Highlighter } from "shiki";
import { useTheme } from "../hooks/useTheme";

let highlighterPromise: Promise<Highlighter> | null = null;

function getHighlighter() {
if (!highlighterPromise) {
highlighterPromise = createHighlighter({
themes: ["github-light", "github-dark"],
langs: ["typescript", "json"]
});
}
return highlighterPromise;
}

export interface CodeSection {
title: string;
description: string;
code: string;
}

interface CodeExplanationProps {
sections: CodeSection[];
}

export function HighlightedCode({
code,
lang = "typescript"
}: {
code: string;
lang?: "typescript" | "json";
}) {
const { resolvedMode } = useTheme();
const theme = resolvedMode === "dark" ? "github-dark" : "github-light";
const [html, setHtml] = useState("");

useEffect(() => {
let cancelled = false;
getHighlighter().then((h) => {
if (cancelled) return;
setHtml(h.codeToHtml(code, { lang, theme }));
});
return () => {
cancelled = true;
};
}, [code, lang, theme]);

if (!html) {
return (
<pre className="font-mono text-sm p-3 rounded-md bg-kumo-base border border-kumo-fill overflow-x-auto leading-relaxed">
<code>{code}</code>
</pre>
);
}

return (
<div
className="rounded-md overflow-x-auto text-sm [&_pre]:p-3 [&_pre]:!bg-kumo-base [&_pre]:!leading-relaxed border border-kumo-fill"
dangerouslySetInnerHTML={{ __html: html }}
/>
);
}

export function HighlightedJson({ data }: { data: unknown }) {
const code = JSON.stringify(data, null, 2);
return <HighlightedCode code={code} lang="json" />;
}

export function CodeExplanation({ sections }: CodeExplanationProps) {
return (
<Surface className="rounded-lg ring ring-kumo-line mt-6">
<div className="flex items-center gap-2 px-4 py-3 border-b border-kumo-line">
<CodeIcon size={18} className="text-kumo-subtle" />
<Text variant="secondary" size="sm" bold>
How it works
</Text>
</div>

<div className="px-4 pb-4 space-y-6 pt-4">
{sections.map((section, i) => (
<div key={i}>
<Text bold size="sm">
{section.title}
</Text>
<p className="text-sm text-kumo-subtle mt-1 mb-3 leading-relaxed">
{section.description}
</p>
<HighlightedCode code={section.code} />
</div>
))}
</div>
</Surface>
);
}
6 changes: 6 additions & 0 deletions examples/playground/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
export { LogPanel, type LogEntry } from "./LogPanel";
export { ConnectionStatus } from "./ConnectionStatus";
export { LocalDevBanner } from "./LocalDevBanner";
export {
CodeExplanation,
HighlightedCode,
HighlightedJson,
type CodeSection
} from "./CodeExplanation";
70 changes: 64 additions & 6 deletions examples/playground/src/demos/ai/ChatDemo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,47 @@ import {
} from "@phosphor-icons/react";
import { Streamdown } from "streamdown";
import { DemoWrapper } from "../../layout";
import { ConnectionStatus } from "../../components";
import {
ConnectionStatus,
CodeExplanation,
type CodeSection
} from "../../components";
import { useUserId } from "../../hooks";

const codeSections: CodeSection[] = [
{
title: "Create an AI chat agent",
description:
"Extend AIChatAgent to get built-in message history, streaming, and tool support. Override onChatMessage to handle incoming messages with any AI provider.",
code: `import { AIChatAgent } from "@cloudflare/ai-chat";

class ChatAgent extends AIChatAgent<Env> {
async onChatMessage(onFinish) {
const result = streamText({
model: workersai("@cf/zai-org/glm-4.7-flash"),
messages: this.messages,
onFinish,
});
return result.toDataStreamResponse();
}
}`
},
{
title: "Connect with useAgentChat",
description:
"The useAgentChat hook gives you a complete chat interface — messages array, input handling, submit function, and streaming status. It manages the full lifecycle over WebSocket.",
code: `import { useAgent } from "agents/react";
import { useAgentChat } from "@cloudflare/ai-chat/react";

const agent = useAgent({ agent: "chat-agent", name: "my-chat" });

const { messages, input, setInput, handleSubmit, isLoading } =
useAgentChat(agent, {
onError: (err) => console.error(err),
});`
}
];

function ReasoningTrace({
text,
state
Expand Down Expand Up @@ -111,7 +149,7 @@ function ChatUI() {
"connected" | "connecting" | "disconnected"
>("connecting");
const [input, setInput] = useState("");
const messagesEndRef = useRef<HTMLDivElement>(null);
const messagesContainerRef = useRef<HTMLDivElement>(null);

const agent = useAgent({
agent: "ChatAgent",
Expand Down Expand Up @@ -140,7 +178,8 @@ function ChatUI() {
const isConnected = connectionStatus === "connected";

useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
const el = messagesContainerRef.current;
if (el) el.scrollTop = el.scrollHeight;
}, [messages]);

const send = useCallback(() => {
Expand All @@ -153,12 +192,30 @@ function ChatUI() {
return (
<DemoWrapper
title="AI Chat"
description="Chat with an AI agent powered by Workers AI. Messages persist across reconnections."
description={
<>
Extend{" "}
<code className="text-xs bg-kumo-fill px-1 py-0.5 rounded">
AIChatAgent
</code>{" "}
to get a full chat backend with built-in message history, streaming,
and tool support. On the client,{" "}
<code className="text-xs bg-kumo-fill px-1 py-0.5 rounded">
useAgentChat
</code>{" "}
gives you messages, input handling, and streaming status out of the
box. Messages persist in the agent's Durable Object, so they survive
page refreshes and reconnections. Try asking about the weather.
</>
}
statusIndicator={<ConnectionStatus status={connectionStatus} />}
>
<div className="flex flex-col h-full max-w-3xl">
{/* Messages area */}
<div className="flex-1 overflow-y-auto mb-4 space-y-4">
<div
ref={messagesContainerRef}
className="flex-1 overflow-y-auto mb-4 space-y-4"
>
{messages.length === 0 && (
<Empty
icon={<ChatCircleDotsIcon size={32} />}
Expand Down Expand Up @@ -289,7 +346,7 @@ function ChatUI() {
);
})}

<div ref={messagesEndRef} />
<div />
</div>

{/* Input area */}
Expand Down Expand Up @@ -342,6 +399,7 @@ function ChatUI() {
</form>
</div>
</div>
<CodeExplanation sections={codeSections} />
</DemoWrapper>
);
}
Expand Down
Loading
Loading