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/streams-legacy-table-shared.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"agents": patch
---

`Streams` rechecks that the v1 chunk table still exists before using it. Two Streams instances on one object, such as a host's own and the one `ThinkHarness` keeps, each remembered the table separately, so after one folded the last v1 rows and dropped the table the other failed its next append with `no such table`.
7 changes: 7 additions & 0 deletions .changeset/think-harness.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"agents": patch
---

Add `ThinkHarness` from the new experimental `agents/harness/think` entry point. It runs Think's agent loop as a Lifecycle capability with the same shape as `PiHarness` (`prompt`, `submit`, `wait`, `abort`, `sessions`, `session(id)`), and implements the shared harness interface Channels serves.

The harness owns its storage: transcripts in the Sessions tables, with `branches()`, `search()` and `compact()` on each session, and in-flight model output in the Streams tables, so a host installs only the harness. Each session has one Lifecycle wake job, so a turn interrupted by an eviction picks up from its last durable write: a cut-short model call is rebuilt from its stream and continued in the same message, and a cut-short tool call is reported to the model, or rerun when the tool carries `recovery: "rerun"`. `ThinkChat` serves a session over Think's `useAgentChat` WebSocket protocol. See [Think harness](https://github.com/cloudflare/agents/blob/main/docs/agents/harnesses/think.md).
215 changes: 215 additions & 0 deletions docs/agents/harnesses/think.md

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Coordinate the upstream documentation update

The docs rules require a matching cloudflare-docs update for new reference pages. Coordinate that separate update before publishing this API.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/agents/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ The differentiator is not "we have durable state" — it is what happens when a
- [Models (Experimental)](./models.md) - `createAI` — one AI SDK provider for Workers AI and third-party catalog models, same string space
- [Models for pi-ai (Beta)](./models-pi-ai.md) - `createAI` for pi-ai: Workers AI ids and third-party models through AI Gateway, as a pi-ai provider
- [Pi harness (Beta)](./harnesses/pi.md) - Host pi-durable sessions in a Durable Object with durable storage and lifecycle wakeups
- [Think harness (Experimental)](./harnesses/think.md) - Run Think's agent loop as a Lifecycle capability, with transcripts in Sessions, output in Streams, and turns that survive eviction
- TODO: [RAG (Retrieval Augmented Generation)](./rag.md) - Vector search with Vectorize
- [Sessions (Experimental)](./sessions.md) - Durable message trees, streamed history, compaction, search, and lossless attachment offload
- [Context (Experimental)](./context.md) - System-prompt blocks, frozen prompts, writable/searchable/loadable providers, and their tools
Expand Down
1 change: 1 addition & 0 deletions examples/next/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ lands.
| [`dynamic-agents`](./dynamic-agents) | This PR | A supervisor runs user-submitted code as facets: isolated storage, supervised abort, code upgrades over stable state |
| [`harnesses/codex`](./harnesses/codex) | This PR | A static Codex Rust/Wasm loop composed as a Lifecycle capability, using LanguageModelV4 and Shell Workspace |
| [`harnesses/pi`](./harnesses/pi) | This PR | Experimental: pi-durable sessions on `agents/harness/pi`, with Workspace tools and a JavaScript `exec` from `@cloudflare/computer` |
| [`harnesses/think`](./harnesses/think) | This PR | Experimental: Think's agent loop on a plain `DurableObject` with `agents/harness/think`, served to `useAgentChat` by `ThinkChat` |
| [`harnesses/self-modifying`](./harnesses/self-modifying) | This PR | A Lifecycle capability runs editable harness revisions in fresh Dynamic Workers with trusted System tools and auto-discovered Custom tools |
| [`models`](./models) | This PR | One `createAI` per framework (AI SDK and pi-ai): `@cf/` ids for Workers AI, vendor models routed through AI Gateway |
| [`channels`](./channels) | This PR | Agents on different harnesses served through `Channels.forHarness` and one `ChannelGateway`, to a browser client and terminal clients |
Expand Down
65 changes: 65 additions & 0 deletions examples/next/harnesses/think/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Think harness

An experimental example of `ThinkHarness` from `agents/harness/think`: Think's
agent loop as a Lifecycle capability on a plain Durable Object, served to
`useAgentChat` by `ThinkChat`.

`src/server.ts` composes three capabilities:

- `ThinkHarness` keeps the transcript and runs turns with the AI SDK. It
keeps each model call's output in storage while it streams, and one
Lifecycle job per session wakes the object after an eviction.
- `WebSockets` accepts the browser's connection.
- `ThinkChat` speaks Think's `cf_agent_chat_*` protocol over it, so the
client is the stock `useAgentChat` from `agents/chat/react`.

```ts
export class ThinkAgent extends DurableObject<Env> {
readonly harness = new ThinkHarness({
model: createAI({ binding: this.env.AI })("@cf/moonshotai/kimi-k2.7-code"),
tools: { getWeather, sendNotification, getUserTimezone }
});
readonly webSockets = new WebSockets();
readonly chat = new ThinkChat({
harness: this.harness,
webSockets: this.webSockets
});
readonly lifecycle = Lifecycle.install(this)
.use(this.harness)
.use(this.webSockets)
.use(this.chat);
}
```

Each browser gets its own chat, named by a random id kept in
`localStorage`, so visitors to a deployed copy do not share a transcript.

The tools show the three ways a tool call runs:

| Tool | Runs | After an eviction cuts a call short |
| ------------------ | ----------------------- | -------------------------------------------------- |
| `getWeather` | on the server | runs again, because it carries `recovery: "rerun"` |
| `sendNotification` | on the server, approved | reported to the model as interrupted (the default) |
| `getUserTimezone` | in the browser | waits for the browser's result, as it did before |

## Run locally

```sh
pnpm install
pnpm run start
```

The example uses the remote Workers AI binding and may incur Workers AI
usage. It needs no API key.

## Try

- "What is the weather in Lisbon?" runs a server tool.
- "Notify Sam that lunch is ready" asks for your approval first.
- "What time zone am I in?" runs a tool in the browser.
- Refresh the page while an answer streams. The client reconnects and the
stream picks up where it was.

## Learn more

- [Think harness docs](../../../../docs/agents/harnesses/think.md)
14 changes: 14 additions & 0 deletions examples/next/harnesses/think/env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/* eslint-disable */
// Generated by Wrangler by running `wrangler types --include-runtime=false env.d.ts` (hash: 0cb7d4d3f8bfd65f4a85db3f24bf892b)
interface __BaseEnv_Env {
AI: Ai;
ThinkAgent: DurableObjectNamespace<import("./src/server").ThinkAgent>;
}
declare namespace Cloudflare {
interface GlobalProps {
mainModule: typeof import("./src/server");
durableNamespaces: "ThinkAgent";
}
interface Env extends __BaseEnv_Env {}
}
interface Env extends __BaseEnv_Env {}
21 changes: 21 additions & 0 deletions examples/next/harnesses/think/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/favicon.ico" />
<title>Think harness</title>
<script>
(() => {
const stored = localStorage.getItem("theme");
const mode = stored || "light";
document.documentElement.setAttribute("data-mode", mode);
document.documentElement.style.colorScheme = mode;
})();
</script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/client.tsx"></script>
</body>
</html>
37 changes: 37 additions & 0 deletions examples/next/harnesses/think/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "@cloudflare/agents-next-think-harness-example",
"description": "Experimental: Think's agent loop on a plain Durable Object with agents/harness/think, served to useAgentChat",
"private": true,
"type": "module",
"version": "0.0.0",
"scripts": {
"start": "vite dev",
"build": "vite build",
"deploy": "vite build && wrangler deploy",
"typecheck": "tsc --noEmit",
"types": "wrangler types env.d.ts --include-runtime false"
},
"dependencies": {
"@cloudflare/kumo": "^2.6.0",
"@phosphor-icons/react": "^2.1.10",
"@streamdown/code": "^1.1.1",
"agents": "*",
"ai": "^7.0.0",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"streamdown": "^2.5.0",
"zod": "^4.5.4"
},
"devDependencies": {
"@cloudflare/vite-plugin": "1.62.3",
"@cloudflare/workers-types": "^5.20260729.1",
"@tailwindcss/vite": "^4",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.3",
"tailwindcss": "^4.3.2",
"typescript": "^6.0.3",
"vite": "^8.1.0",
"wrangler": "^4.145.0"
}
}
Binary file added examples/next/harnesses/think/public/favicon.ico
Binary file not shown.
Loading
Loading