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

Add the workspace tools for two more agent libraries, so the same
`read`, `ls`, `find`, `grep`, `write`, `edit`, `delete`, and `exec`
tools now work whichever of three libraries an agent is built on.
`@cloudflare/computer/tools/pi` serves pi, which keeps the tool list
apart from the code that runs the tools, so it returns both the
declarations and a dispatcher. `@cloudflare/computer/tools/tanstack`
serves TanStack AI, which runs the tools itself and takes them as a
list, so a list is what it returns; `format: "object"` keys them by
name when a single tool has to be reached. Each library is an optional
peer dependency, so installing one does not pull in the others.

Each library declares its own tools, so it can use that library's own
features directly. What they share is the workspace logic underneath:
the executors, their schemas and descriptions, and the options that
decide which tools exist. `@cloudflare/computer/tools` is unchanged for
AI SDK callers.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,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`](examples/pi) — a one-shot [pi](https://github.com/earendil-works/pi)
agent: a minimal loop that asks a model, runs the workspace tools it asks
for, and repeats until it is done.
- [`examples/tanstack-ai`](examples/tanstack-ai) — the same one-shot agent on
[TanStack AI](https://tanstack.com/ai), where `chat()` owns 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
149 changes: 147 additions & 2 deletions docs/09_tool_interface.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,78 @@
# 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` ships ready-made tools for agents that use a `Workspace`. Three agent SDKs are supported from one implementation:

| SDK | Entrypoint | 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` | `createPiTools` |
| [TanStack AI](https://tanstack.com/ai) (`@tanstack/ai`) | `@cloudflare/computer/tools/tanstack` | `createTanStackTools` |

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.assets` for publishing generated files when an assets publisher is configured.

Every factory takes the same options and produces the same tools with the same names, descriptions, schemas, and caps. Only the returned shape differs, because each SDK wants a different one. Each SDK's package is an optional peer dependency: importing one adapter does not require the other two to be installed.

## Shared logic, separate tools

Each library gets its own directory, and declares its own tools in that
library's own terms:

```
tools/
common/ ← executors, schemas, descriptions, options, helpers
ai-sdk/ ← createAITools (ai)
pi/ ← createPiTools (pi-ai, TypeBox)
tanstack-ai/ ← createTanStackTools (@tanstack/ai)
```

`common/` is deliberately not a tool abstraction. It holds the workspace
logic underneath the tools — reading a file by line and byte offset,
applying an edit and rendering its diff, buffering a command's output —
along with each tool's Zod schema and description text, the shared
options, and a few helpers with no library in them. A provider directory
imports those executors and writes out its own tool declarations.

That means the same tool is declared three times, once per library. The
duplication is intentional: it is a few lines of declaration each, and
in exchange every provider can use its library's own features directly
instead of through a lowest-common-denominator layer.

Where the libraries genuinely differ, each provider handles it in its
own file:

| Concern | AI SDK | pi | TanStack AI |
| --- | --- | --- | --- |
| Schema | Zod, passed through | converted to JSON Schema for TypeBox | Zod, passed through (Standard Schema) |
| Execution | `execute` on the tool | caller's loop, via the returned `execute` dispatcher | `execute` on the tool |
| Streaming `exec` | progressive tool results | terminal snapshot | terminal snapshot, optional custom events |
| Images and PDFs | typed `file` output part | base64 `image` block (PDFs degrade to text) | base64 payload plus media type |
| Argument enforcement | schema validation | provider-side constrained sampling | schema validation |
| Result shape | `toModelOutput` | `toolResult` content blocks | `outputSchema` |
| Approval and discovery | — | — | `needsApproval`, `lazy` |

Because each provider owns its declarations, it can act on what its
library supports without asking the others to agree. pi marks `edit` and
`write` for provider-side constrained sampling because their arguments
are easy to malform; TanStack marks the mutating tools `needsApproval`
and can emit progressive `exec` output as custom events; the AI SDK
hands an async iterable straight to `execute` so streaming works with no
draining at all.

A fourth library is a new directory next to these three. It imports the
executors and schemas from `common/` and declares its tools however that
library wants them.

## What ships

| Export | Purpose |
| --- | --- |
| `createAITools` | Create the default AI SDK `ToolSet` for a Workspace. |
| `createPiTools` | Create pi tool declarations plus their executor. |
| `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 @@ -23,8 +83,9 @@ The tools wrap three Workspace surfaces:
| `createExecTool` | Run a command through a configured Workspace backend. |
| `createPublishTool` | Publish a workspace file through `workspace.assets`. |
| `WorkspaceFileStore` | Adapt `workspace.fs` to the store used by file tools. |
| `resolveToolOptions` | Resolve the shared options and tool-inclusion rules. |

`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 factory 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`.

## Wiring up

Expand Down Expand Up @@ -55,6 +116,90 @@ export class Agent {

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

### pi

pi splits a tool into data and execution: `Context.tools` carries declarations with TypeBox `parameters`, and the caller's own agent loop runs the calls. `createPiTools` returns both halves so they cannot drift apart.

```ts
import { createPiTools } from "@cloudflare/computer/tools/pi";
import { builtinModels } from "@earendil-works/pi-ai/providers/all";

const { tools, execute } = createPiTools({ workspace });
const models = builtinModels();
const model = models.getModel("anthropic", "claude-sonnet-4-5")!;

const context = {
systemPrompt: "You are a coding agent working in /workspace.",
messages: [{ role: "user", content: "Summarize the README.", timestamp: Date.now() }],
tools,
};

// One turn of the caller's loop.
const message = await models.complete(model, context);
context.messages.push(message);

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

`execute` validates the call's arguments against the tool's schema and returns pi `toolResult` content, reporting a bad call or a failed tool as `isError: true` so the model can retry instead of the loop throwing. The Zod schemas are converted to plain JSON Schema for TypeBox, so a field with a default stays optional for the model and pi applies the default during validation.

Tools whose arguments are structurally fussy — `read`, `write`, and `edit`, which carry byte offsets and long verbatim strings — are declared with pi's `constrainedSampling`, so a provider that supports it enforces the schema during sampling and a malformed `edit` never reaches the tool. Strict enforcement requires a closed schema in which every property is listed, so those declarations set `additionalProperties: false`, mark each optional field nullable, and the dispatcher drops the resulting top-level nulls before validation.

The default is `strict: "prefer"`, which falls back to ordinary tool calling on a provider that cannot enforce a schema. Pass `constrainedSampling: "require"` to fail the request instead, when the caller pins a model known to support it, or `false` to opt out and get the open schemas:

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

### TanStack AI

A TanStack tool is a plain object whose `inputSchema` is a Standard Schema, which Zod implements, so the schemas are passed through with no conversion. The tools come back as a list, which is what every TanStack entry point takes: `chat({ tools })`, `mergeAgentTools`, and `createToolRegistry` all want an array. Pass `format: "object"` to get them keyed by name instead, for reaching one tool directly, such as to adjust a single tool before the call.

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

export async function POST(request: Request) {
const { messages } = await request.json();
const abortController = new AbortController();

const tools = createTanStackTools({
workspace,
shell: { defaultBackend: "shell", backends: { shell: { description: "Worker shell." } } },
approve: "mutating",
});

return toServerSentEventsResponse(
chat({
adapter: anthropicText("claude-sonnet-4-5"),
messages,
tools,
abortController,
}),
);
}
```

`approve` marks tools that pause for confirmation through TanStack's `needsApproval`. Pass a name list, or `"mutating"` to gate every tool that changes workspace state so the list does not have to be restated as the tool set grows. `lazy` takes the same shape and marks tools to withhold from the prompt until TanStack's lazy discovery asks for them, which keeps a workspace tool set out of the system prompt for an agent whose file work is occasional.

The tools that have one fixed result shape — `write`, `edit`, `delete`, and `publish` — also carry an `outputSchema`, which TanStack validates client-side and threads into its typed hooks. Paged tools like `ls` omit it. The schema covers the error branch as well as success, because TanStack validates every return against it: a success-only schema would replace a real failure reason with a validation complaint.

Aborting the chat run through its `abortController` kills a running `exec`, because TanStack hands each tool call the run's abort signal. A TanStack tool settles on one value, so `exec` returns the run's terminal snapshot; set `streamEventName` to also forward each running snapshot through `emitCustomEvent` as it arrives, for a live view of a command's output.

### Shared options

Pass `shell` only when the Workspace has matching backend ids:

```ts
Expand Down
46 changes: 46 additions & 0 deletions examples/pi/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# pi 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`](../../docs/09_tool_interface.md): `read`,
`ls`, `find`, `grep`, `write`, `edit`, `delete`, and `exec`.

[`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
```

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.

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`.
22 changes: 22 additions & 0 deletions examples/pi/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "@example/computer-pi",
"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.",
"scripts": {
"dev": "wrangler dev",
"deploy": "wrangler deploy",
"typecheck": "tsc --noEmit",
"build:types": "wrangler types"
},
"dependencies": {
"@cloudflare/computer": "*",
"@earendil-works/pi-ai": "^0.85.1",
"zod": "^4.4.3"
},
"devDependencies": {
"typescript": "^6.0.3",
"wrangler": "^4.130.0"
}
}
Loading
Loading