Cloudflare Agents ワークショップ

Cloudflare Developer Advocate
Yusuke Wada


https://workshop.yusuke.run/cloudflare-agents


Yusuke Wada

::right::

me


やること

CloudflareでAIエージェントをつくろう!


アジェンダ


方針


AIエージェントとは?


AIエージェントは、AIを活用したコンピュータプログラムで、明示的な指示がなくても自律的にタスクを実行して人間のユーザーを支援することができます。

https://www.cloudflare.com/ja-jp/learning/ai/what-is-agentic-ai/


AIの変遷

AIの変遷


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


AIエージェントの活用シーン


コーディングエージェント

::right::

coding agent


Cloudflare Agents


AIエージェントに必要な3つの要素

ai


Cloudflareには3つが揃っている

ai


3つの要素


推論モデル


AIプロダクト


Workers AI


80以上のモデル

::right::

models


簡単で柔軟な呼び出し

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',
      },
    ],
  }
)

サンドボックス


サンドボックスの必要性


3つのサンドボックス


Sandboxes


Pythonコードの実行

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,
    })
  },
}

Gitの操作

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 })
  },
}

プレビューURLの発行

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)

    // ...
  },
}

Browser Run


Broser Sessions


Quick Actions

const response = await env.BROWSER.quickAction('screenshot', {
  url: 'https://example.com',
})

Browser Runをサンドボックスとして使う


Dynamic Workers


Workersの実行環境

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

実行環境


AIエージェントが動く環境


Agents SDK


Agents SDKとは?


AIエージェントに欲しいもの


Agents SDKでできること


Agents SDKのコード例


Agentsクラス

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
  }
}

useAgent

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>
    </>
  )
}

バニラJavaScriptからの接続

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.jsonwrangler.jsoncnameを変更


bun create hono@latest my-counter

choice


依存関係のインストール

bun add agents hono-agents

Skillsのインストール

必要に応じて

bunx skills add cloudflare/skills
bunx skills add yusukebe/hono-skill

型定義の作成

bun run cf-typegen

Viteの設定

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の設定

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

デモ: カウンター


SS


デモ: memo2task


memo2task


Workers AIを使う

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
  }
}

Schedule tasksを使う

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 })
  }
}

デモ: Screenshot Workflow


Screenshot Workflow


Browser Runを使う

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 },
})

Workflowsを使う

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

実用的なエージェントへ

これまでの技術を組み合わせてしてエージェントを作る


デモ: Meshi Agent


meshi agent


使った技術


その他の例


Let It Slide

::right::

let it slide


AIを使った開発


Skills


AIに優しいドキュメントサイト

https://developers.cloudflare.com/

docs


まとめ


CloudflareでAIエージェントを構築しましょう!


Cloudflare Workers Tech Talksについて

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

workers tech talks

← → / space ・ f: fullscreen