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
11 changes: 11 additions & 0 deletions .changeset/exec-tool-options.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@cloudflare/computer": minor
---

`createAITools` takes an `exec` option that lists the backends the model can use, keyed by backend id: `exec: { "worker-javascript": { description: "Use for data work." } }`. Leave it out to use every backend the Workspace has. `{}` exposes a backend with nothing beyond its own description, and `exec: {}` means no exec tool. `createExecTool` takes the same map as `backends`, and `defaultBackend` goes away: with more than one backend the model must name one on every call.

`WorkerShellBackend` and `CloudflareContainerBackend` now describe themselves to the model, as `WorkerJavaScriptBackend` does, so the default needs no descriptions. A backend that says nothing gets a one-line default instead of an error.

`shell` still works and is deprecated. `shell: { backends }` becomes `exec: backends`, and its `defaultBackend` is ignored. Output limits stay on `createExecTool`.

`createAITools` moves to its own entry point, `@cloudflare/computer/tools/ai-sdk`. `@cloudflare/computer/tools` keeps the individual `create*Tool` functions and `WorkspaceFileStore`. Change `import { createAITools } from "@cloudflare/computer/tools"` to `from "@cloudflare/computer/tools/ai-sdk"`.
4 changes: 2 additions & 2 deletions .changeset/exec-tool-single-backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
"@cloudflare/computer": minor
---

The `exec` tool offers only the arguments that can work. With one backend there is no `backend` argument, the tool always runs there, `defaultBackend` becomes optional, and the description talks about what that backend does rather than how to choose one. `input` appears only when a configured backend accepts it.
The `exec` tool offers only the arguments that can work. With one backend there is no `backend` argument, the tool always runs there, and the description talks about what that backend does rather than how to choose one. `input` appears only when a configured backend accepts it.

Each backend's entry now adds what the backend says about itself, read through `workspace.runtime.describe(id)`. For `WorkerJavaScriptBackend` that is its source language and every module code can import, so `shell: { backends: { "worker-javascript": {} } }` is enough and the module list the model reads cannot drift from `modules`. A backend `description` is required only for a backend that does not describe itself.
Each backend's entry now adds what the backend says about itself, read through `workspace.runtime.describe(id)`. For `WorkerJavaScriptBackend` that is its source language and every module code can import, so the module list the model reads cannot drift from `modules`.
44 changes: 17 additions & 27 deletions docs/09_tool_interface.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# 09. Tool interface (agents)

`@cloudflare/computer/tools` ships ready-made [AI SDK](https://github.com/vercel/ai) tools for agents that use a `Workspace`.
`@cloudflare/computer/tools/ai-sdk` ships `createAITools()`, a ready-made [AI SDK](https://github.com/vercel/ai) tool set for agents that use a `Workspace`. The individual `create*Tool` functions and `WorkspaceFileStore` come from `@cloudflare/computer/tools`.

The tools wrap three Workspace surfaces:

- `workspace.fs` for file reads, writes, edits, searches, listings, and deletion;
- `workspace.runtime.exec` for command execution when the caller opts in;
- `workspace.runtime.exec` for running commands and code on the Workspace's backends;
- `workspace.assets` for publishing generated files when an assets publisher is configured.

## What ships
Expand All @@ -24,13 +24,13 @@ The tools wrap three Workspace surfaces:
| `createPublishTool` | Publish a workspace file through `workspace.assets`. |
| `WorkspaceFileStore` | Adapt `workspace.fs` to the store used by file tools. |

`createAITools()` always names its tools `read`, `ls`, `find`, `grep`, `write`, `edit`, and `delete`. `exec` appears when the caller supplies `shell` options. `publish` appears when assets are configured. In read-only mode the set is `read`, `ls`, `find`, and `grep`.
`createAITools()` always names its tools `read`, `ls`, `find`, `grep`, `write`, `edit`, and `delete`. `exec` appears when the Workspace has a backend, unless you pass `exec: {}`. `publish` appears when assets are configured. In read-only mode the set is `read`, `ls`, `find`, and `grep`.

## Wiring up

```ts
import { Workspace } from "@cloudflare/computer";
import { createAITools } from "@cloudflare/computer/tools";
import { createAITools } from "@cloudflare/computer/tools/ai-sdk";

export class Agent {
workspace: Workspace;
Expand All @@ -55,29 +55,18 @@ export class Agent {

Pass the returned AI SDK `ToolSet` to `generateText`, `streamText`, or an agent framework hook such as `getTools()`.

Pass `shell` only when the Workspace has matching backend ids. With one backend, `exec` has no `backend` argument and always runs there:
`exec` lists the backends the model can use, keyed by backend id. Leave it out to use every backend.

```ts
const tools = createAITools({
createAITools({ workspace }); // every backend
createAITools({ workspace, exec: { "worker-javascript": {} } }); // just this one
createAITools({
workspace,
shell: { backends: { "worker-javascript": {} } },
exec: { "worker-javascript": { description: "Use for data work." } }, // with your own text
});
```

With more than one, pass `defaultBackend` and the model picks a backend per call:

```ts
const tools = createAITools({
workspace,
shell: {
defaultBackend: "shell",
backends: {
shell: { description: "Fast Worker shell with built-in text commands." },
container: { description: "Full Linux userland in a Cloudflare Container." },
},
},
});
```
Each backend describes itself, and a `description` you pass comes first. `exec: {}` means no exec tool. With one backend, `exec` has no `backend` argument and always runs there. With several, the model must name a backend on every call; there is no default.

## `createAITools`

Expand All @@ -89,7 +78,7 @@ createAITools({
read?,
write?,
edit?,
shell?,
exec?,
});
```

Expand All @@ -101,7 +90,8 @@ createAITools({
| `read` | default caps | Options passed to `createReadTool`. |
| `write` | default caps | Options passed to `createWriteTool`. |
| `edit` | default caps | Options passed to `createEditTool`. |
| `shell` | omitted | Options passed to `createExecTool`. |
| `exec` | every backend | Backend id to `{ description? }`. `{}` omits `exec`. |
| `shell` | omitted | Deprecated. `{ backends }` becomes `exec: backends`; `defaultBackend` is ignored. |

## `read`

Expand Down Expand Up @@ -253,21 +243,21 @@ The tool uses forced removal, so deleting a missing path succeeds. Set `recursiv

## `exec`

`exec` is opt-in. It calls `workspace.runtime.exec` with the configured backend and streams bounded output.
`exec` calls `workspace.runtime.exec` on the chosen backend and streams bounded output. `createExecTool({ workspace, backends?, maxBytes?, streamMaxBytes? })` takes the same `backends` as the `exec` option, plus output limits.

Each backend's entry in the tool description joins two parts: the `description` you pass, and what the backend says about itself (`backend.description`, read through `workspace.runtime.describe(id)`). `WorkerJavaScriptBackend` describes its source language and every module code can import, so `{ "worker-javascript": {} }` is enough and the list stays in step with `modules`. A backend that does not describe itself needs a `description`. Describe capabilities and startup cost in plain language.
Each backend's entry in the tool description joins two parts: your text, if any, and what the backend says about itself (`backend.description`, read through `workspace.runtime.describe(id)`). `WorkerJavaScriptBackend` describes its source language and every module code can import, so the list stays in step with `modules`. `WorkerShellBackend` and `CloudflareContainerBackend` describe their command sets and startup cost. A backend that says nothing gets a one-line default, so add text for a custom backend.

The tool offers only the arguments that can work:

| Backends | Arguments |
| --- | --- |
| One shell backend | `command`, `cwd`, `env` |
| One callable backend | `command`, `cwd`, `env`, `input` |
| More than one | `command`, `cwd`, `backend`, `env`, plus `input` when any is callable. `defaultBackend` is required. |
| More than one | `command`, `cwd`, `backend` (required), `env`, plus `input` when any is callable |

A `backend` value the model sends anyway is dropped when only one backend is configured. The output still names the backend that ran.

Wire this tool carefully: it executes arbitrary shell commands inside the configured backend. Treat its output as untrusted text when including it in later model input. Omit `shell` or use `readonly: true` when command execution is not part of the agent's job.
Wire this tool carefully: it executes arbitrary shell commands inside the configured backend. Treat its output as untrusted text when including it in later model input. Pass `exec: {}` or `readonly: true` when command execution is not part of the agent's job, and list backends explicitly when the Workspace has one the model should not use directly.

## `publish`

Expand Down
7 changes: 4 additions & 3 deletions docs/10_project_layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,9 +175,10 @@ produces the Node SEA single-file binary at

## Tools

AI SDK tools (`read`, `write`, `edit`, `ls`, optional `exec`, and
optional `publish`) ship from the `@cloudflare/computer/tools` subpath
rather than a separate package, under
AI SDK tools (`read`, `write`, `edit`, `ls`, `exec`, and optional
`publish`) ship from the package rather than a separate one:
`createAITools()` from `@cloudflare/computer/tools/ai-sdk`, and the
individual `create*Tool` functions from `@cloudflare/computer/tools`. They live under
[`packages/computer/src/tools/`](../packages/computer/src/tools/). See
[09. Tool Interface (Agents)](./09_tool_interface.md).

Expand Down
6 changes: 2 additions & 4 deletions docs/17_isolate_javascript.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,10 +264,8 @@ this.workspace = new Workspace({
],
});

const tools = createAITools({
workspace: this.workspace,
shell: { backends: { "worker-javascript": {} } },
});
// Offer only the JavaScript backend; the container is reached through ws:container.
const tools = createAITools({ workspace: this.workspace, exec: { "worker-javascript": {} } });
```

```js
Expand Down
3 changes: 2 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ It provides:
- Pluggable execution backends selected through `workspace.runtime`: a Cloudflare Container shell, a just-bash Dynamic Worker, or an isolated ECMAScript-module Dynamic Worker.
- Isolated JavaScript with structured input/results, durable relative imports, configured libraries, durable `node:fs/promises`, host modules such as `ws:git` and `ws:container`, and managed execution records.
- Workspace constructable without a backend, for filesystem-only use cases.
- Out-of-the-box AI SDK tools for `@cloudflare/agents` through `@cloudflare/computer/tools`.
- Out-of-the-box AI SDK tools for `@cloudflare/agents` through `createAITools()` in `@cloudflare/computer/tools/ai-sdk`.

It comes with the following limitations:

Expand Down Expand Up @@ -52,6 +52,7 @@ The package ships several entrypoints:
| `@cloudflare/computer/modules/git` | `createGitModule()` for `ws:git`: confined Git from isolate JavaScript. |
| `@cloudflare/computer/modules/artifacts` | `createArtifactsModule()` for `ws:artifacts`: Artifacts from isolate JavaScript. |
| `@cloudflare/computer/tools` | AI SDK tools for agents: read, write, edit, ls, optional exec, and optional publish. |
| `@cloudflare/computer/tools/ai-sdk` | `createAITools()`: the AI SDK tool set for a Workspace. |

A consumer that only uses the container backend never imports the
worker subpath, so the just-bash payload tree-shakes away.
Expand Down
2 changes: 1 addition & 1 deletion examples/celld/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ the message or `CELLD_EXPECT` to use a different expected phrase.

## Workspace tools

The agent receives these tools from `@cloudflare/computer/tools`:
The agent receives these tools from `createAITools()` in `@cloudflare/computer/tools/ai-sdk`:

| Tool | Purpose |
| --- | --- |
Expand Down
35 changes: 16 additions & 19 deletions examples/celld/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
type WorkspaceRuntimeLoader,
withWorkspace,
} from "@cloudflare/computer";
import { createAITools } from "@cloudflare/computer/tools";
import { createAITools } from "@cloudflare/computer/tools/ai-sdk";
import { routeAgentRequest } from "agents";
import { convertToModelMessages, isStepCount, streamText } from "ai";
import { createWorkersAI } from "workers-ai-provider";
Expand Down Expand Up @@ -49,24 +49,21 @@ export class CelldAgent extends withWorkspace(CelldAgentBase, (self) => {
assets: false,
...(this.bindings.LOADER
? {
shell: {
defaultBackend: CELLD_JAVASCRIPT_BACKEND_ID,
backends: {
[CELLD_JAVASCRIPT_BACKEND_ID]: {
description: [
"Runs a complete JavaScript module in a celld Dynamic Worker with structured input and output.",
"Pass module source, not a filename or bare script. The module must have a default export. Export a function to receive `(input, ctx)` and return structured output.",
"",
"```js",
"export default async function main(input, ctx) {",
' console.log("cwd:", ctx.cwd);',
" return { received: input };",
"}",
"```",
"",
"The loaded worker cannot access the Workspace filesystem. Use read, write, edit, ls, find, grep, and delete outside exec.",
].join("\n"),
},
exec: {
[CELLD_JAVASCRIPT_BACKEND_ID]: {
description: [
"Runs a complete JavaScript module in a celld Dynamic Worker with structured input and output.",
"Pass module source, not a filename or bare script. The module must have a default export. Export a function to receive `(input, ctx)` and return structured output.",
"",
"```js",
"export default async function main(input, ctx) {",
' console.log("cwd:", ctx.cwd);',
" return { received: input };",
"}",
"```",
"",
"The loaded worker cannot access the Workspace filesystem. Use read, write, edit, ls, find, grep, and delete outside exec.",
].join("\n"),
},
},
}
Expand Down
7 changes: 5 additions & 2 deletions examples/mcp/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,11 @@ describe("Computer Code Mode MCP", () => {
});
const file = await codemode.read({ path: "/workspace/message.txt" });
const listing = await codemode.ls({ path: "/workspace" });
const shell = await codemode.exec({ command: "pwd" });
const git = await codemode.exec({ command: "git init && git status --short" });
const shell = await codemode.exec({ command: "pwd", backend: "worker-shell" });
const git = await codemode.exec({
command: "git init && git status --short",
backend: "worker-shell",
});
return {
content: file.content,
listed: listing.entries.some((entry) => entry.name === "message.txt"),
Expand Down
43 changes: 20 additions & 23 deletions examples/mcp/src/server.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DynamicWorkerExecutor } from "@cloudflare/codemode";
import { codeMcpServer } from "@cloudflare/codemode/mcp";
import type { WorkspaceClient } from "@cloudflare/computer";
import { createAITools } from "@cloudflare/computer/tools";
import { createAITools } from "@cloudflare/computer/tools/ai-sdk";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { ToolSet } from "ai";

Expand All @@ -11,29 +11,26 @@ export async function createComputerMCPServer(workspace: WorkspaceClient, loader
const tools = createAITools({
workspace,
assets: false,
shell: {
backends: {
"worker-shell": {
description:
"just-bash in an isolated Dynamic Worker. Starts quickly, " +
"does not boot a container, and has no ambient outbound network. " +
"Use it for common shell commands, quick file inspection, and " +
"text transformations. Its built-in git command supports clone, " +
"status, diff, and log; clone accepts HTTPS URLs through the " +
"durable workspace. Prefer the dedicated read, write, and edit " +
"tools for file operations. Cannot run npm, Node.js, Python, " +
"package managers, or arbitrary native binaries.",
},
"container-shell": {
description:
"Full Debian Linux in a Cloudflare Container with Node.js, npm, " +
"git, package management, native binaries, and outbound network. " +
"Use it for dependency installation, builds, tests, or commands " +
"that worker-shell cannot run. Cold starts more slowly because " +
"the container must boot; prefer worker-shell for simple tasks.",
},
exec: {
"worker-shell": {
description:
"just-bash in an isolated Dynamic Worker. Starts quickly, " +
"does not boot a container, and has no ambient outbound network. " +
"Use it for common shell commands, quick file inspection, and " +
"text transformations. Its built-in git command supports clone, " +
"status, diff, and log; clone accepts HTTPS URLs through the " +
"durable workspace. Prefer the dedicated read, write, and edit " +
"tools for file operations. Cannot run npm, Node.js, Python, " +
"package managers, or arbitrary native binaries.",
},
"container-shell": {
description:
"Full Debian Linux in a Cloudflare Container with Node.js, npm, " +
"git, package management, native binaries, and outbound network. " +
"Use it for dependency installation, builds, tests, or commands " +
"that worker-shell cannot run. Cold starts more slowly because " +
"the container must boot; prefer worker-shell for simple tasks.",
},
defaultBackend: "worker-shell",
},
});

Expand Down
1 change: 0 additions & 1 deletion examples/rlm/worker/executor-tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ export function createExecutorTool(
"Callable isolated JavaScript. The command must be a complete ES module with a default async function.",
},
},
defaultBackend: backend,
maxBytes: 16 * 1024,
streamMaxBytes: 16 * 1024,
});
Expand Down
9 changes: 5 additions & 4 deletions examples/think/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ would use, so no bespoke HTTP route or transport is involved.

[think]: https://www.npmjs.com/package/@cloudflare/think
[workspace]: ../../packages/computer
[tools]: ../../packages/computer/src/tools
[tools]: ../../packages/computer/src/tools/ai-sdk.ts
[aisdk7]: https://vercel.com/blog/ai-sdk-7

## Shape
Expand Down Expand Up @@ -53,9 +53,10 @@ model, a Workspace, and the workspace tools.
## Tools

The tools come from `createAITools()` in
[`@cloudflare/computer/tools`][tools]. This example enables the file
tools and opts into `exec` by passing a shell backend description; it
does not configure the assets publisher, so `publish` is not offered.
[`@cloudflare/computer/tools/ai-sdk`][tools]. This example offers the
file tools and an `exec` tool over both backends, each of which
describes itself to the model. It does not configure the assets
publisher, so `publish` is not offered.

| Tool | What it does |
| ------- | --------------------------------------------------------- |
Expand Down
Loading
Loading