Cloudflare Developer Advocate
Yusuke Wada
https://workshop.yusuke.run/cloudflare-agents
::right::

CloudflareでAIエージェントをつくろう!
AIエージェントは、AIを活用したコンピュータプログラムで、明示的な指示がなくても自律的にタスクを実行して人間のユーザーを支援することができます。
https://www.cloudflare.com/ja-jp/learning/ai/what-is-agentic-ai/

AIが考えるだけじゃなくて行動する
::right::



::right::

const result = env.AI.run(
'@cf/meta/llama-3.3-70b-instruct-fp8-fast',
{
messages: [
{
role: 'user',
content: 'What is the origin of the phrase Hello World',
},
],
}
)
import { getSandbox } from '@cloudflare/sandbox'
export { Sandbox } from '@cloudflare/sandbox'
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const sandbox = getSandbox(env.Sandbox, 'user-123')
const result = await sandbox.exec('python --version')
return Response.json({
output: result.stdout,
exitCode: result.exitCode,
success: result.success,
})
},
}
import { getSandbox } from '@cloudflare/sandbox'
export { Sandbox } from '@cloudflare/sandbox'
export default {
async fetch(request: Request, env: Env) {
const sandbox = getSandbox(env.Sandbox, 'agent-session-47')
await sandbox.gitCheckout('https://github.com/org/repo', {
targetDir: '/workspace',
depth: 1,
})
return sandbox.exec('npm', ['test'], { stream: true })
},
}
import { getSandbox } from '@cloudflare/sandbox'
export { Sandbox } from '@cloudflare/sandbox'
export default {
async fetch(request: Request, env: Env) {
const { hostname } = new URL(request.url)
const sandbox = getSandbox(env.Sandbox, 'agent-session-47')
await sandbox.startProcess('python -m http.server 8000')
const exposed = await sandbox.exposePort(8000, { hostname })
console.log(exposed.url)
// ...
},
}
const response = await env.BROWSER.quickAction('screenshot', {
url: 'https://example.com',
})
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const worker = env.LOADER.load({
compatibilityDate: '2026-05-23',
mainModule: 'src/index.js',
modules: {
'src/index.js': `
export default {
fetch(request) {
return new Response("Hello from a dynamic Worker");
},
};
`,
},
globalOutbound: null,
})
const entrypoint = worker.getEntrypoint()
return entrypoint.fetch(request)
},
}
export default {
fetch: async (request, env) => {
const code = await request.text()
const worker = env.LOADER.load({
compatibilityDate: '2026-06-01',
mainModule: 'src/index.js',
modules: {
'src/index.js': code,
},
})
const entrypoint = worker.getEntrypoint()
return entrypoint.fetch('http://dummy')
},
}
curl -XPOST \
-d "export default { fetch: () => new Response('from curl') }" \
http://localhost:8787
import { Agent, callable } from 'agents'
export class CounterAgent extends Agent {
initialState = { count: 0 }
@callable()
increment() {
this.setState({ count: this.state.count + 1 })
return this.state.count
}
}
import { useState } from 'react'
import { useAgent } from 'agents/react'
export function CounterWidget() {
const [count, setCount] = useState(0)
const agent = useAgent({
agent: 'CounterAgent',
onStateUpdate: (state) => setCount(state.count),
})
return (
<>
{count}
<button onClick={() => agent.stub.increment()}>+</button>
</>
)
}
import { AgentClient } from 'agents/client'
const agent = new AgentClient({
host: 'localhost:5173',
agent: 'CounterAgent',
})
await agent.call('increment')
今回はHonoでReactを使う雛形を使う
git clone git@github.com:yusukebe/hono-react-starter.git my-counter
cd my-counter
bun install
その後、package.jsonとwrangler.jsoncのnameを変更
bun create hono@latest my-counter

bun add agents hono-agents
必要に応じて
bunx skills add cloudflare/skills
bunx skills add yusukebe/hono-skill
bun run cf-typegen
vite.config.ts
import { cloudflare } from '@cloudflare/vite-plugin'
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
import ssrPlugin from 'vite-ssr-components/plugin'
import agents from 'agents/vite'
export default defineConfig({
build: {
minify: true,
},
plugins: [
agents(),
cloudflare(),
ssrPlugin({
hotReload: {
ignore: ['./src/client/**/*.tsx'],
},
}),
react(),
],
})
src/agents/counter.ts
import { Agent, callable } from 'agents'
export type CounterState = {
count: number
}
export class CounterAgent extends Agent<
CloudflareBindings,
CounterState
> {
initialState: CounterState = { count: 0 }
@callable()
increment() {
this.setState({ count: this.state.count + 1 })
return this.state.count
}
@callable()
decrement() {
this.setState({ count: this.state.count - 1 })
return this.state.count
}
}
src/index.tsx
import { agentsMiddleware } from 'hono-agents'
export { CounterAgent } from './agents/couter'
// ...
app.use('*', agentsMiddleware())
wrangler.jsonc
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "my-counter",
"compatibility_date": "2025-08-03",
"main": "./src/index.tsx",
"compatibility_flags": ["nodejs_compat"],
"durable_objects": {
"bindings": [
{
"name": "CounterAgent",
"class_name": "CounterAgent"
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["CounterAgent"]
}
]
}
bun run cf-typegen
import { useState } from 'react'
import { useAgent } from 'agents/react'
import type { CounterAgent, CounterState } from '../agents/counter'
export function Counter() {
const [count, setCount] = useState(0)
const agent = useAgent<CounterAgent, CounterState>({
agent: 'CounterAgent',
onStateUpdate: (state) => setCount(state.count),
})
return (
<>
{count}
<button onClick={() => agent.stub.increment()}>+</button>
<button onClick={() => agent.stub.decrement()}>-</button>
</>
)
}
bun run dev
bun run deploy


src/agents/task.ts
export class TaskAgent extends Agent<CloudflareBindings, TaskState> {
// ...
@callable()
async addMemo(memo: string, timeZone: string) {
const { title, notifyAt } = await this.parseMemo(memo, timeZone)
// ...
}
private async parseMemo(memo: string, timeZone: string): Promise<{ title: string; notifyAt: string }> {
const workersai = createWorkersAI({ binding: this.env.AI })
let result: { title: string; notifyAt: string } | undefined
await generateText({
model: workersai('@cf/qwen/qwen3-30b-a3b-fp8'),
tools: {
createTask: tool({
description: 'Submit the final task with its title and notify time.',
inputSchema: z.object({
title: z.string().describe('The task title'),
notifyAt: z.string().describe('ISO 8601 time to notify, with UTC offset')
}),
execute: (task) => {
result = task
return { ok: true }
}
})
},
stopWhen: stepCountIs(2),
system: // メモからタイトル、通知する時間をAIに取ってきてもらう指示
prompt: `The current time is ${currentTimeInZone(timeZone)}.\nMemo: ${memo}`
})
return result
}
}
src/agents/task.ts
export class TaskAgent extends Agent<CloudflareBindings, TaskState> {
@callable()
async addMemo(memo: string, timeZone: string) {
const { title, notifyAt } = await this.parseMemo(memo, timeZone)
const id = crypto.randomUUID()
const dueAt = Date.parse(notifyAt)
const delaySeconds = Math.max(
1,
Math.round((dueAt - Date.now()) / 1000)
)
// ...
await this.schedule(delaySeconds, 'notify', { id })
}
}

const response = await env.BROWSER.quickAction('screenshot', { url })
const data = await response.arrayBuffer()
const contentType =
response.headers.get('content-type') ?? 'image/png'
const object = await env.BUCKET.put(url, data, {
httpMetadata: { contentType },
})
export class ScreenshotWorkflow extends WorkflowEntrypoint<Bindings> {
async run(
event: WorkflowEvent<Payload>,
step: WorkflowStep
): Promise<ScreenshotResult> {
// ...
const result = await step.do(
'capture-screenshot',
{
retries: {
limit: 3,
delay: '5 seconds',
backoff: 'exponential',
},
timeout: '1 minute',
},
async (): Promise<ScreenshotResult> => {
// ...
}
)
await step.do('verify-stored', async () => {
// ...
})
return result
}
}
import { Hono } from 'hono'
import { md5 } from 'hono/utils/crypto'
export { ScreenshotWorkflow } from './workflows/screenshot'
const app = new Hono<{ Bindings: Bindings }>()
app.get('/screenshot', async (c) => {
const url = c.req.query('url')
const data = await c.env.BUCKET.get(url)
if (data) {
return c.body(await data.arrayBuffer())
}
try {
const instance = await c.env.WORKFLOW.create({
id: `workflow-${await md5(url)}`,
params: { url },
})
return c.text(`Instance ${instance.id} is processing`)
} catch (err) {
if (
err instanceof Error &&
err.message.includes('already_exists')
) {
return c.text(`Instance already exists`)
}
}
})
export default app
これまでの技術を組み合わせてしてエージェントを作る

::right::

https://developers.cloudflare.com/

Cloudflare Workers / 開発者プラットフォームに特化したミートアップ
