integrations · think
Think + Effect
The smallest Think DO whose custom tool body is an Effect program.
integrations/think/worker.ts
// integrations/think — the smallest Think DO whose custom tool body is an
// Effect program.
//
// The tool registration is pure Think (via the `ai` SDK's `tool()` factory).
// The tool's `execute` function runs an `Effect.gen` block and resolves with
// `Effect.runPromise`. That one line is the only seam between Think (the host)
// and Effect (the body).
//
// Mirrors think-snippets/examples/effect-hello.
// https://github.com/acoyfellow/think-snippets/tree/main/examples/effect-hello
import { Think } from "@cloudflare/think"
import { getAgentByName } from "agents"
import { tool } from "ai"
import { createWorkersAI } from "workers-ai-provider"
import { z } from "zod"
import { Effect } from "effect"
export interface Env {
AI: Ai
Greeter: DurableObjectNamespace<Greeter>
}
interface UIMessageChunk {
type: string
delta?: string
text?: string
}
interface StreamCallback {
onEvent: (json: string) => void
onDone?: () => void
onError?: (message: string) => void
}
// The Effect program — pure, no Think, no `ai` SDK, no env.
// Inputs are plain values; output is a string.
const greetEffect = (name: string) =>
Effect.gen(function* () {
yield* Effect.sleep("50 millis") // proves the Effect actually runs
if (!name.trim()) {
return yield* Effect.fail(new Error("name is required"))
}
return `Hello, ${name.trim()}! Welcome to Think + Effect.`
}).pipe(Effect.timeout("5 seconds"))
export class Greeter extends Think<Env> {
getModel() {
return createWorkersAI({ binding: this.env.AI })("@cf/moonshotai/kimi-k2.6")
}
getSystemPrompt() {
return [
"You are a greeting assistant.",
"When the user gives you a name to greet, you MUST call the `greet` tool with that name.",
"Reply with exactly the tool result and nothing else."
].join(" ")
}
getTools() {
return {
greet: tool({
description: "Greet a person by name. Returns a friendly greeting string.",
inputSchema: z.object({
name: z.string().min(1).max(120).describe("The name of the person to greet.")
}),
// The seam between Think (the host) and Effect (the body).
execute: async ({ name }) => {
const greeting = await Effect.runPromise(greetEffect(name))
return { greeting }
}
})
}
}
}
The smallest Think DO whose custom tool body is an Effect program.
Think handles the DO state, the chat protocol, the tool-call lifecycle, and the assistant-answer streaming. The custom tool's body is an Effect.gen(...) block executed with one Effect.runPromise(...) call. That single line is the only seam.
The seam
execute: async ({ name }) => {
const greeting = await Effect.runPromise(greetEffect(name))
return { greeting }
}
The execute function is just an async function — Think doesn't care what runs inside it. Run an Effect program; the tool result reaches the assistant answer like any other tool.
Composition
|
What Think gives you |
What Effect gives you |
| DO state |
per-session SQLite, chat memory, the tool-call protocol |
— |
| Tool body |
tool registration via tool() from the ai SDK |
typed errors, timeouts, retries, structured concurrency |
| Scaling up |
more tools, hooks (afterToolCall), audit |
scaling the body of each tool — concurrency, branching, structured output, all composable |
If you want to see what real Effect agents look like — concurrency, retry, streaming, approval flows, typed errors, MCP — see the five agents in this repo.
Live in the Think repo
This file is also shipped as a runnable example in think-snippets/examples/effect-hello, where it deploys via Alchemy under the personal-account guard, runs an end-to-end probe that drives a real chat turn against the deployed Worker, and asserts the Effect-baked literal reaches the assistant answer.
# from acoyfellow/think-snippets, with CLOUDFLARE_PERSONAL_* set
bash examples/effect-hello/run-e2e.sh