Skip to content
Draft
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
17 changes: 7 additions & 10 deletions docs/01_vfs.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,13 @@ Workspace where `fs` works against the local SQLite store but
`shell` throws.

`WorkspaceOptions` includes the storage handle, optional backends,
clock, session id, mounts, observer, git identity, assets, artifacts,
and `useThink`. There is no `root` or `sandbox` field on the host
facade — sandbox wiring lives behind a `WorkspaceBackend`.

Set `useThink: true` when assigning the Workspace to
`Think.workspace`. This adds Think's string-oriented filesystem
compatibility methods (`readFile`, `readFileBytes`, `writeFile`,
`readDir`, `rm`, `glob`, `mkdir`, and `stat`) to that Workspace and to
clients returned by `getWorkspace()`, while leaving the primary API on
`workspace.fs`.
clock, session id, mounts, observer, git identity, assets, and
artifacts. There is no `root` or `sandbox` field on the host facade —
sandbox wiring lives behind a `WorkspaceBackend`.

Agent integrations consume the filesystem through `workspace.fs`.
Use `createAITools` from `@cloudflare/computer/tools` for the standard
AI SDK file tools.

Illustrative layout (nothing below `/` is auto-created beyond
`ROOT_INODE` itself):
Expand Down
13 changes: 6 additions & 7 deletions examples/think/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,13 @@
* a CloudflareContainerBackend (`"container"`) for full Linux
* userland through computerd. This mirrors examples/container while
* keeping the chat surface unchanged.
* - `useThink: true` adds the string-based compatibility surface
* Think expects; the cast promotes it from optional to present.
* `workspaceBash` is off because `@cloudflare/computer/tools`
* provides the `exec` tool.
* - Think consumes `workspace.fs` directly, while
* `@cloudflare/computer/tools` provides the complete file and
* `exec` tool set.
*/

import {
type DurableObjectStorageLike,
type ThinkWorkspaceCompatibility,
Workspace,
WorkspaceProxy,
WorkspaceServiceProxy,
Expand Down Expand Up @@ -88,6 +86,8 @@ export class Assistant extends withWorkspaceContainer(AssistantBase) {
* shell. Passing `{ backend: "container" }` routes a call to computerd in
* the Cloudflare Container.
*/
// The installed Think release still types workspace as its legacy
// root-level filesystem shape. Runtime tools use workspace.fs.
override workspace = new Workspace({
storage: this.ctx.storage as unknown as DurableObjectStorageLike,
backends: [
Expand All @@ -99,8 +99,7 @@ export class Assistant extends withWorkspaceContainer(AssistantBase) {
}),
this.#containerBackend,
],
useThink: true,
}) as Workspace & ThinkWorkspaceCompatibility;
}) as Workspace & AssistantBase["workspace"];

/** Forwarded by WorkspaceProxy for computerd's outbound /ws upgrade. */
override async fetch(request: Request): Promise<Response> {
Expand Down
60 changes: 39 additions & 21 deletions examples/tutorial/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ with a link to the PDF.
POST /prompt ──► RecipeAgent
│ fetch_url https://openstove.org/... (host)
│ write /workspace/card.md (host)
bash pandoc card.md -o card.pdf (container)
exec pandoc card.md -o card.pdf (container)
R2 ──► signed link, good for a day
```
Expand All @@ -26,7 +26,7 @@ of the workspace. The `write` tool runs on the host, in the durable
object, and writes through the `Workspace` into durable object storage.
The container sees the same file on its FUSE mount at `/workspace`, so
`pandoc` reads it as an ordinary file. The PDF `pandoc` writes syncs
back the other way when the `bash` call finishes, so the finished file
back the other way when the `exec` call finishes, so the finished file
can be published straight from the workspace.

The finished code is [one file](src/index.ts). The rest of this page
Expand Down Expand Up @@ -86,11 +86,12 @@ wrangler r2 bucket create recipe-cards
npm install @cloudflare/computer @cloudflare/think agents ai zod
```

- `@cloudflare/computer` is the filesystem, the container backend, and
the assets client that publishes to R2.
- `@cloudflare/think` provides the agent loop and its file, shell, and
fetch tools. `agents` provides `getAgentByName` for reaching an
instance by name; `ai` and `zod` satisfy Think's peer dependencies.
- `@cloudflare/computer` provides the filesystem, container backend,
AI SDK file and execution tools, and the assets client that publishes
to R2.
- `@cloudflare/think` provides the agent loop and fetch tools. `agents`
provides `getAgentByName` for reaching an instance by name; `ai` and
`zod` satisfy Think's peer dependencies.

## 3. Create the Dockerfile

Expand Down Expand Up @@ -132,9 +133,9 @@ works in both places.
## 4. Give Think a Computer workspace

The durable object owns a `CloudflareContainerBackend`, which is the
container the workspace is mounted in. The `Workspace` instance enables
Computer's Think-compatible methods so Think's built-in tools use the
same filesystem as the container.
container the workspace is mounted in. Think receives the Computer
`Workspace`; agent tools use its `workspace.fs` and `workspace.runtime`
facades directly.

```ts
import {
Expand All @@ -144,23 +145,22 @@ import {
import { Think } from "@cloudflare/think";
import {
type DurableObjectStorageLike,
type ThinkWorkspaceCompatibility,
Workspace,
} from "@cloudflare/computer";

class RecipeBase extends Think<Env> {}

export class RecipeAgent extends withWorkspaceContainer(RecipeBase) {
readonly #backend = new CloudflareContainerBackend({
id: "container",
container: () => this,
workspace: { binding: "RecipeAgent", id: this.ctx.id.toString() },
});

override workspace = new Workspace({
storage: this.ctx.storage as unknown as DurableObjectStorageLike,
backends: [this.#backend],
useThink: true,
}) as Workspace & ThinkWorkspaceCompatibility;
});

override async fetch(request: Request): Promise<Response> {
return new URL(request.url).pathname === "/ws"
Expand All @@ -178,13 +178,17 @@ hand `/ws` to the backend before the base class sees it.

## 5. Hook the workspace up to the agent

Think already has file and shell tools. The Computer workspace makes
those tools use the same filesystem as the container:
`write` calls Computer's host-side filesystem, while `bash` calls the
container shell. Think's fetch tool gets an allowlist of one host, so
the agent can read openstove.org and nothing else.
Disable Think's legacy shell tool and return Computer's complete tool
set from `getTools()`. `write` calls the host-side filesystem, while
`exec` runs against the container backend. Think's fetch tool gets an
allowlist of one host, so the agent can read openstove.org and nothing
else.

```ts
import { createAITools } from "@cloudflare/computer/tools";
import type { ToolSet } from "ai";

override workspaceBash = false;
override maxSteps = 10;
override fetchTools = {
allowlist: ["https://openstove.org/**"],
Expand All @@ -196,6 +200,20 @@ override getModel() {
return "@cf/zai-org/glm-5.2";
}

override getTools(): ToolSet {
return createAITools({
workspace: this.workspace,
shell: {
defaultBackend: "container",
backends: {
container: {
description: "Cloudflare Container with full Linux userland, including pandoc and typst.",
},
},
},
});
}

override getSystemPrompt() {
return [
"You turn a cooking request into a one-page PDF recipe card.",
Expand All @@ -209,13 +227,13 @@ override getSystemPrompt() {
" and numbered Method steps. End with the source page URL spelled out,",
" not a markdown link: the card gets printed, and a link prints as its",
" text alone.",
"3. Convert it with `bash`: `pandoc /workspace/card.md -o /workspace/card.pdf --pdf-engine=typst`.",
"3. Convert it with `exec`: `pandoc /workspace/card.md -o /workspace/card.pdf --pdf-engine=typst`.",
"4. Reply with one sentence naming the recipe you picked.",
].join("\n");
}
```

Nothing copies files between `write` and `bash`: the write goes into
Nothing copies files between `write` and `exec`: the write goes into
durable object storage and the container reads it back out of the mount,
and the PDF `pandoc` leaves behind travels the same road in reverse.

Expand Down Expand Up @@ -351,7 +369,7 @@ curl -X POST http://localhost:8787/prompt \
```

The first request may be slow: the container has to boot before the
first `bash` runs. The link points at R2 rather than at the worker, so
first `exec` runs. The link points at R2 rather than at the worker, so
it works the same whether the worker runs locally or deployed.

`wrangler deploy` works against any account with Workers AI and
Expand Down
37 changes: 26 additions & 11 deletions examples/tutorial/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,27 @@
// POST /prompt ──► RecipeAgent (Think + Computer)
// │ fetch_url openstove.org (host)
// │ write /workspace/card.md (host)
// │ bash pandoc card.md -o card.pdf (container)
// │ exec pandoc card.md -o card.pdf (container)
// ▼
// R2 ──► signed link, good for a day
//
// The write and the pandoc run touch one filesystem: the host writes
// through the Workspace, the container sees the same bytes on its
// FUSE mount, and the PDF the container produces is readable back on
// the host once the shell command finishes.
// the host once the exec command finishes.
//
// README.md walks through building this file from an empty directory.

import {
type DurableObjectStorageLike,
type ThinkWorkspaceCompatibility,
Workspace,
WorkspaceProxy,
} from "@cloudflare/computer";
import { type DurableObjectStorageLike, Workspace, WorkspaceProxy } from "@cloudflare/computer";
import { createAssets } from "@cloudflare/computer/assets";
import {
CloudflareContainerBackend,
withWorkspaceContainer,
} from "@cloudflare/computer/backends/container";
import { createAITools } from "@cloudflare/computer/tools";
import { Think } from "@cloudflare/think";
import { getAgentByName } from "agents";
import type { ToolSet } from "ai";

// Carries container egress back to the durable object. The runtime
// binds it by name, so it has to appear in the worker's module graph.
Expand All @@ -38,6 +35,7 @@ export { WorkspaceProxy };
class RecipeBase extends Think<Env> {}

export class RecipeAgent extends withWorkspaceContainer(RecipeBase) {
override workspaceBash = false;
override maxSteps = 10;
override fetchTools = {
allowlist: ["https://openstove.org/**"],
Expand All @@ -46,15 +44,17 @@ export class RecipeAgent extends withWorkspaceContainer(RecipeBase) {
};

readonly #backend = new CloudflareContainerBackend({
id: "container",
container: () => this,
workspace: { binding: "RecipeAgent", id: this.ctx.id.toString() },
});

// The installed Think release still types workspace as its legacy
// root-level filesystem shape. Runtime tools use workspace.fs.
override workspace = new Workspace({
storage: this.ctx.storage as unknown as DurableObjectStorageLike,
backends: [this.#backend],
useThink: true,
}) as Workspace & ThinkWorkspaceCompatibility;
}) as Workspace & RecipeBase["workspace"];

override getModel() {
return "@cf/zai-org/glm-5.2";
Expand All @@ -73,11 +73,26 @@ export class RecipeAgent extends withWorkspaceContainer(RecipeBase) {
" and numbered Method steps. End with the source page URL spelled out,",
" not a markdown link: the card gets printed, and a link prints as its",
" text alone.",
"3. Convert it with `bash`: `pandoc /workspace/card.md -o /workspace/card.pdf --pdf-engine=typst`.",
"3. Convert it with `exec`: `pandoc /workspace/card.md -o /workspace/card.pdf --pdf-engine=typst`.",
"4. Reply with one sentence naming the recipe you picked.",
].join("\n");
}

override getTools(): ToolSet {
return createAITools({
workspace: this.workspace,
shell: {
defaultBackend: "container",
backends: {
container: {
description:
"Cloudflare Container with full Linux userland, including pandoc and typst.",
},
},
},
});
}

override async fetch(request: Request): Promise<Response> {
return new URL(request.url).pathname === "/ws"
? this.#backend.handleFetch(request)
Expand Down
6 changes: 3 additions & 3 deletions packages/computer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -461,9 +461,9 @@ await ws.ready();
const stub = ws.stub(); // crosses the Workers-RPC boundary
```

When assigning a workspace to a Think agent's `workspace`, pass
`useThink: true` so Think's compatibility methods are added alongside
`workspace.fs` and `workspace.runtime`.
Agent frameworks should consume files through `workspace.fs`. Use
`createAITools` from `@cloudflare/computer/tools` for the standard AI
SDK file tools.

### Durable pending-sync retries

Expand Down
Loading