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
5 changes: 5 additions & 0 deletions .changeset/pi-ai-tanstack-ai-tools.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudflare/computer": minor
---

Add Workspace tool sets for pi (`createPiTools` from `@cloudflare/computer/tools/pi-ai`) and TanStack AI (`createTanStackTools` from `@cloudflare/computer/tools/tanstack-ai`); see [the tool interface docs](https://github.com/cloudflare/computer/blob/main/docs/09_tool_interface.md).
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,12 @@ jobs:
- name: mcp
workspace: "@example/computer-mcp"
path: examples/mcp
- name: pi-ai
workspace: "@example/computer-pi-ai"
path: examples/pi-ai
- name: tanstack-ai
workspace: "@example/computer-tanstack-ai"
path: examples/tanstack-ai
- name: think
workspace: "@cloudflare/example-think"
path: examples/think
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ public surface. Each is a Worker workspace with its own README.
- [`examples/rlm`](examples/rlm) — shows how generated JavaScript can read long
context from a Computer Workspace, call bounded model workers, and reduce their
structured results with code.
- [`examples/pi-ai`](examples/pi-ai) — a one-shot [pi](https://github.com/earendil-works/pi)
agent. Its loop asks the model, runs the workspace tools it asked for,
and repeats until the model stops asking.
- [`examples/tanstack-ai`](examples/tanstack-ai) — the same one-shot agent on
[TanStack AI](https://tanstack.com/ai), where `chat()` runs the loop.
- [`examples/think`](examples/think) — a [`@cloudflare/think`](https://www.npmjs.com/package/@cloudflare/think)
chat agent that uses the workspace as its working directory, reachable
from a terminal.
Expand Down
85 changes: 82 additions & 3 deletions docs/09_tool_interface.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
# 09. Tool interface (agents)

`@cloudflare/computer/tools` ships ready-made [AI SDK](https://github.com/vercel/ai) tools for agents that use a `Workspace`.
Computer ships a ready-made tool set for agents that use a `Workspace`, once for each of three agent libraries:

| Library | Entry point | Factory |
| --- | --- | --- |
| [AI SDK](https://github.com/vercel/ai) (`ai`) | `@cloudflare/computer/tools` | `createAITools` |
| [pi](https://github.com/earendil-works/pi) (`@earendil-works/pi-ai`) | `@cloudflare/computer/tools/pi-ai` | `createPiTools` |
| [TanStack AI](https://tanstack.com/ai) (`@tanstack/ai`) | `@cloudflare/computer/tools/tanstack-ai` | `createTanStackTools` |

All three take the same options and build the same tools, with the same names, descriptions, schemas, and limits. Only the shape they return differs. Each entry point imports only `zod` and its own library's types, so a pi agent never loads `ai` and an AI SDK agent never loads pi. The individual AI SDK `create*Tool` functions and `WorkspaceFileStore` also come from `@cloudflare/computer/tools`.

The tools wrap three Workspace surfaces:

Expand All @@ -13,6 +21,8 @@ The tools wrap three Workspace surfaces:
| Export | Purpose |
| --- | --- |
| `createAITools` | Create the default AI SDK `ToolSet` for a Workspace. |
| `createPiTools` | Create pi tool declarations and the function that runs a pi tool call. |
| `createTanStackTools` | Create the TanStack AI tool list for a Workspace. |
| `createReadTool` | Stream text by line and pass images or PDFs to capable models. |
| `createWriteTool` | Write a whole file with a UTF-8 byte cap. |
| `createEditTool` | Apply atomic targeted replacements and return a unified diff. |
Expand All @@ -24,7 +34,7 @@ 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`.
Every tool set 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`.

## Wiring up

Expand Down Expand Up @@ -70,7 +80,74 @@ const tools = createAITools({
});
```

## `createAITools`
`createPiTools` and `createTanStackTools` take `shell` the same way.

## pi

pi keeps tool declarations apart from the code that runs them. `Context.tools` carries declarations with JSON Schema `parameters`, and the caller's own loop runs each call. `createPiTools` returns both, so they cannot drift apart.

```ts
import { createPiTools } from "@cloudflare/computer/tools/pi-ai";

const { tools, execute } = createPiTools({ workspace });

const message = await models.complete(model, { systemPrompt, messages, tools });
messages.push(message);

for (const block of message.content) {
if (block.type !== "toolCall") continue;
const { content, isError } = await execute(block);
messages.push({
role: "toolResult",
toolCallId: block.id,
toolName: block.name,
content,
isError,
timestamp: Date.now(),
});
}
```

`execute` checks the call's arguments against the tool's schema and returns pi `toolResult` content. A bad call or a failed tool comes back as `isError: true`, so the model can retry and the loop does not throw. pi describes tool parameters with TypeBox, which also accepts plain JSON Schema, so the Zod schemas are converted to JSON Schema and pi needs nothing else. A field with a default stays optional for the model.

`read`, `write`, and `edit` carry byte offsets and long verbatim strings, so they ask for pi's `constrainedSampling`. A provider that supports it enforces the schema while sampling, and a malformed `edit` never reaches the tool. The declarations stay open. pi closes a schema itself when the provider supports strict mode, making every field required and the optional ones nullable. `execute` drops a null on an optional field that does not accept one, and keeps a null the tool accepts, such as `exec`'s `input`.

The default is `"prefer"`, which falls back to ordinary tool calling on a provider that cannot enforce a schema. `"require"` fails the request instead, for a pinned model known to support it. `false` turns it off and keeps the schemas open:

```ts
createPiTools({ workspace, constrainedSampling: "require" });
```

pi tool results carry text and images. An image from `read` comes back as an `image` block; a PDF comes back as text saying it cannot be attached. `exec` returns its final snapshot.

## TanStack AI

A TanStack tool's `inputSchema` is a Standard Schema, which Zod implements, so the schemas pass through unchanged. The tools come back as a list, the shape `chat({ tools })`, `mergeAgentTools`, and `createToolRegistry` take. `format: "object"` keys them by name instead, for reaching one tool directly.

```ts
import { chat, toServerSentEventsResponse } from "@tanstack/ai";
import { createTanStackTools } from "@cloudflare/computer/tools/tanstack-ai";

const abortController = new AbortController();
const tools = createTanStackTools({ workspace, approve: "mutating" });

return toServerSentEventsResponse(chat({ adapter, messages, tools, abortController }));
```

| Option | Default | Notes |
| --- | --- | --- |
| `format` | `"array"` | `"object"` keys the tools by name. |
| `approve` | none | Tool names that pause for TanStack's `needsApproval`, or `"mutating"` for every tool that changes the Workspace. |
| `lazy` | none | Tool names, or `"all"`, to withhold from the prompt until TanStack's lazy discovery asks for them. |
| `streamEventName` | none | Forward each running `exec` snapshot through `emitCustomEvent` under this name. |

`write`, `edit`, `delete`, and `publish` have one fixed result shape, so they also carry an `outputSchema`. It covers failures too, because TanStack validates every return against it, and a success-only schema would replace the real error with a validation complaint. Paged tools such as `ls` have none.

An image or PDF from `read` comes back as a text part plus an `image` or `document` content part, the array shape `chat()` passes to the adapter as multimodal content instead of stringifying it.

Aborting the chat run through its `abortController` kills a running `exec`. A TanStack tool settles on one value, so `exec` returns its final snapshot.

## Options

```ts
createAITools({
Expand All @@ -94,6 +171,8 @@ createAITools({
| `edit` | default caps | Options passed to `createEditTool`. |
| `shell` | omitted | Options passed to `createExecTool`. |

`createPiTools` and `createTanStackTools` take the same options, plus their own listed above.

## `read`

```ts
Expand Down
15 changes: 11 additions & 4 deletions docs/10_project_layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,10 +175,17 @@ 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
[`packages/computer/src/tools/`](../packages/computer/src/tools/). See
Agent tools (`read`, `write`, `edit`, `ls`, optional `exec`, and optional
`publish`) ship from the package rather than a separate one, with one
entry point per agent library: `createAITools()` from
`@cloudflare/computer/tools`, `createPiTools()` from
`@cloudflare/computer/tools/pi-ai`, and `createTanStackTools()` from
`@cloudflare/computer/tools/tanstack-ai`. The individual AI SDK
`create*Tool` functions come from `@cloudflare/computer/tools` too. They live under
[`packages/computer/src/tools/`](../packages/computer/src/tools/):
`common/` holds each tool's schema, description, and executor with no
agent library in it, and `ai-sdk/`, `pi-ai/`, and `tanstack-ai/` wrap
those in each library's tool shape. See
[09. Tool Interface (Agents)](./09_tool_interface.md).

## Git
Expand Down
4 changes: 3 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`, trusted `ws:git` / `ws:artifacts`, 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 agent tools for the AI SDK (`createAITools()` in `@cloudflare/computer/tools`), pi (`createPiTools()` in `@cloudflare/computer/tools/pi-ai`), and TanStack AI (`createTanStackTools()` in `@cloudflare/computer/tools/tanstack-ai`).

It comes with the following limitations:

Expand Down Expand Up @@ -50,6 +50,8 @@ The package ships several entrypoints:
| `@cloudflare/computer/git` | Opt-in isomorphic-git glue for working with checkouts inside the workspace. Bundled lazily, with `pako` replaced by Workers `node:zlib`, and kept out of the default `@cloudflare/computer` graph. |
| `@cloudflare/computer/artifacts` | `createArtifact`, an optionally session-scoped wrapper over the Cloudflare Artifacts Workers binding, plus its argv CLI. |
| `@cloudflare/computer/tools` | AI SDK tools for agents: read, write, edit, ls, optional exec, and optional publish. |
| `@cloudflare/computer/tools/pi-ai` | `createPiTools()`: the same tool set for pi, as declarations plus a function that runs a tool call. |
| `@cloudflare/computer/tools/tanstack-ai` | `createTanStackTools()`: the same tool set for TanStack AI, as the list `chat({ tools })` takes. |

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: 2 additions & 0 deletions examples/pi-ai/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
.wrangler/
50 changes: 50 additions & 0 deletions examples/pi-ai/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# pi-ai agent

A one-shot agent built on [pi](https://github.com/earendil-works/pi). Send it a
task, it works in a durable Workspace, and it replies when it is done.

The whole agent loop is the `run` method in [`src/index.ts`](src/index.ts): ask
the model, run whatever tools it asked for, repeat until it stops asking. pi
keeps the list of tools separate from the code that runs them, so
`createPiTools` hands back both — `tools` to show the model, and `execute` to
run one of its requests.

The workspace tools come from
[`@cloudflare/computer/tools/pi-ai`](../../docs/09_tool_interface.md): `read`,
`ls`, `find`, `grep`, `write`, `edit`, `delete`, and `exec`. `exec` runs on
the Workspace's one backend, a Worker shell, so the model never has to name it.

[`src/workers-ai.ts`](src/workers-ai.ts) teaches pi to reach Workers AI through
the `AI` binding rather than the REST endpoint, so the example needs no API
key. It is lifted from the pi harness example in
[cloudflare/agents](https://github.com/cloudflare/agents).

## Run it

```sh
npm install
npm run dev --workspace @example/computer-pi-ai
```

Then give it something to do:

```sh
curl -X POST http://localhost:8787 \
-H 'content-type: application/json' \
-d '{"task":"Write a haiku about durable objects to /workspace/haiku.txt, then read it back."}'
```

The agent writes the file with the `write` tool and reads it back with `read`,
then says what it did. Ask it to `grep` or run a shell command and it will
reach for those tools instead.

To check the agent loop without a Cloudflare account, `npm run local --workspace @example/computer-pi-ai`
drives it in Node with a scripted model in place of Workers AI.

This uses the remote Workers AI binding and counts against your account's
Workers AI usage. If your Wrangler login has access to more than one account,
set `CLOUDFLARE_ACCOUNT_ID` before starting.

Small models pick tools less reliably than large ones. If the agent replies
without touching a file, say the task more plainly or try a bigger model in
`MODEL`.
16 changes: 16 additions & 0 deletions examples/pi-ai/local-shim.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// `npm run local` loads this before run-local.mjs. @cloudflare/computer
// imports `cloudflare:workers`, which only workerd provides; the local
// run never reaches those classes, so empty stand-ins are enough.
import { register } from "node:module";

const stub =
"export class RpcTarget {} export class WorkerEntrypoint {} export class DurableObject {} export const env = {};";

register(
`data:text/javascript,${encodeURIComponent(`export async function resolve(specifier, context, next) {
if (specifier === "cloudflare:workers") {
return { url: ${JSON.stringify(`data:text/javascript,${stub}`)}, shortCircuit: true };
}
return next(specifier, context);
}`)}`,
);
24 changes: 24 additions & 0 deletions examples/pi-ai/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "@example/computer-pi-ai",
"version": "0.0.0",
"private": true,
"type": "module",
"description": "Example Worker + Durable Object running a one-shot pi agent against a Workspace, with tools from @cloudflare/computer/tools/pi-ai.",
"scripts": {
"dev": "wrangler dev",
"local": "node --import ./local-shim.mjs run-local.mjs",
"deploy": "wrangler deploy",
"typecheck": "tsc --noEmit",
"build:types": "wrangler types"
},
"dependencies": {
"@cloudflare/computer": "*",
"@earendil-works/pi-ai": "^0.99.2",
"zod": "^4.4.3"
},
"devDependencies": {
"@cloudflare/dofs": "*",
"typescript": "^6.0.3",
"wrangler": "^4.137.0"
}
}
Loading
Loading