# AI agents (/docs/ai-agents)



`s3delivery/ai` turns an [`S3Delivery`](/docs/server-client) client into a set of
tools an AI agent can call — `uploadFile`, `listFiles`, `getFileUrl`,
`deleteFile`, and `getUsage`. Spread them into your agent's `tools:` and it can
store, find, link, and clean up files on its own.

The tools are shaped to drop straight into the [Vercel AI SDK](https://ai-sdk.dev)
(`tool()`-compatible: `{ description, inputSchema, execute }`) and work with any
framework that accepts that shape. `ai` is an **optional** peer dependency — this
module never imports it, so the core SDK stays dependency-free apart from `zod`.

```ts
import { S3Delivery } from "s3delivery";
import { createFileTools } from "s3delivery/ai";
import { generateText } from "ai";

const tools = createFileTools(new S3Delivery());

await generateText({
  model,
  tools, // uploadFile, listFiles, getFileUrl, deleteFile, getUsage
  prompt: "Save these notes as notes.txt and give me a shareable link.",
});
```

<Callout type="warn" title="Server-side only">
  `createFileTools` wraps a client that holds your secret API key. Run it on the server (your API
  route, agent backend, or job runner) — never in the browser.
</Callout>

## The tools [#the-tools]

Each tool carries an LLM-friendly description and a zod-validated `inputSchema`,
and returns a compact, JSON-serializable result.

| Tool         | Input                                                       | Result                                   |
| ------------ | ----------------------------------------------------------- | ---------------------------------------- |
| `uploadFile` | `name`, `text` *or* `base64`, `contentType?`, `visibility?` | The stored file (`id`, `name`, `url`, …) |
| `listFiles`  | `limit?`, `offset?`                                         | `{ files: [...] }` with ids and urls     |
| `getFileUrl` | `fileId`, `download?`                                       | `{ url, file }` — a fresh shareable URL  |
| `deleteFile` | `fileId`                                                    | `{ id, deleted: true }`                  |
| `getUsage`   | *(none)*                                                    | Bytes used, file count, plan, and limits |

`uploadFile` takes the bytes as `text` (plain UTF-8, for text files) or `base64`
(for binary). The result objects are summarized — id, name, content type, size,
status, visibility, and url — dropping timestamps the model rarely needs.

## Safety knobs [#safety-knobs]

Two built-in controls let you decide how much an agent can do.

### `readOnly` [#readonly]

Expose only the read tools (`listFiles`, `getFileUrl`, `getUsage`). The mutating
tools (`uploadFile`, `deleteFile`) are omitted entirely. A safe default for
untrusted agents.

```ts
const safe = createFileTools(s3, { readOnly: true });
```

### `requireApproval` + `onApproval` [#requireapproval--onapproval]

Gate mutating tools behind human approval. Pass `true` to gate all mutating tools,
or a per-tool map to gate specific ones.

```ts
const tools = createFileTools(s3, {
  requireApproval: { deleteFile: true },
  onApproval: async ({ tool, input }) =>
    confirm(`Allow agent to ${tool} ${JSON.stringify(input)}?`),
});
```

When a gated tool is invoked, `onApproval` runs first:

* Return `true` → the action runs and its real result is returned.
* Return `false` (or throw) → the tool returns `{ status: "denied", tool, message }`
  and does **not** act.

If you do **not** supply `onApproval`, a gated tool returns
`{ status: "approval_required", tool, message, input }` instead of acting. The
model surfaces that to your app, which can prompt the human out-of-band and re-run
the action on approval — useful when approval is asynchronous (a Slack message, a
UI confirmation) rather than a synchronous `confirm`.

| Option            | Type                                                        | Notes                                                       |
| ----------------- | ----------------------------------------------------------- | ----------------------------------------------------------- |
| `readOnly`        | `boolean`                                                   | Omit the mutating tools entirely.                           |
| `requireApproval` | `boolean \| { uploadFile?: boolean; deleteFile?: boolean }` | Gate mutating tools behind approval.                        |
| `onApproval`      | `({ tool, input }) => boolean \| Promise<boolean>`          | Resolve a gated call: `true` runs it, `false`/throw denies. |

## Wiring into the Vercel AI SDK [#wiring-into-the-vercel-ai-sdk]

`createFileTools` returns a record of tool name → tool, so spread it straight into
`tools:`. It composes with your own tools.

```ts
import { S3Delivery } from "s3delivery";
import { createFileTools } from "s3delivery/ai";
import { generateText, tool } from "ai";
import { z } from "zod";

const s3 = new S3Delivery();

const result = await generateText({
  model,
  tools: {
    ...createFileTools(s3, { requireApproval: { deleteFile: true } }),
    // your own tools alongside the file tools
    getWeather: tool({
      description: "Get the weather for a city.",
      inputSchema: z.object({ city: z.string() }),
      execute: async ({ city }) => fetchWeather(city),
    }),
  },
  prompt: "Generate a CSV of last week's sales and give me a download link.",
});
```

The tools match the object the AI SDK's `tool()` helper returns, so you do not
need to wrap them. They also work with any other framework that accepts the same
`{ description, inputSchema, execute }` shape.

## Use the tools directly [#use-the-tools-directly]

The tools are plain objects, so you can call `execute` yourself — handy for tests
or non-LLM automation.

```ts
const { listFiles, getFileUrl } = createFileTools(s3, { readOnly: true });

const { files } = await listFiles.execute({ limit: 5 });
const { url } = await getFileUrl.execute({ fileId: files[0].id });
```

## A read-only retrieval agent [#a-read-only-retrieval-agent]

A common, safe pattern: let an agent find and link files, but never write.

```ts
const tools = createFileTools(s3, { readOnly: true });

await generateText({
  model,
  tools, // listFiles, getFileUrl, getUsage
  prompt: "Find the latest invoice PDF and give me a shareable link.",
});
```

The agent can discover ids with `listFiles`, then mint a fresh URL with
`getFileUrl` — with no way to upload or delete.
