Skip to content
Draft
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/harness-message-ids.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"agents": minor
---

Breaking (experimental): in the shared harness interface, a `transcript` event replaces `reset`, and `placed` and `done` statuses can report the id a harness gave a user message, which Channels maps to the inbound message's id. `piChannelsHarness` no longer takes a `kv` option. See [Channels](https://github.com/cloudflare/agents/tree/main/packages/agents/src/experimental/channels/docs/CONTEXT.md).
5 changes: 5 additions & 0 deletions .changeset/pi-channels-harness.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"agents": minor
---

Add `piChannelsHarness` to `agents/harness/pi`, which serves `PiHarness` through `Channels.forHarness`, and the pi projection at `agents/experimental/channels/projections/pi`. See [Channels](https://github.com/cloudflare/agents/tree/main/packages/agents/src/experimental/channels/docs/CONTEXT.md).
5 changes: 5 additions & 0 deletions .changeset/remove-ai-sdk-harness.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"agents": minor
---

Breaking (experimental): remove `AiSdkHarness` and the `agents/harness/ai-sdk` entry point, including `createSendMessageTool`; use `ThinkHarness` instead. Its AI SDK message conversions move to `agents/experimental/channels/projections/ai-sdk`. See [Channels](https://github.com/cloudflare/agents/tree/main/packages/agents/src/experimental/channels/docs/CONTEXT.md).
2 changes: 1 addition & 1 deletion docs/agents/fetch-the-web.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const result = streamText({
});
```

If you convert UI messages yourself, pass the same tools to `convertToModelMessages(messages, { tools })`. Without them, the AI SDK sends earlier fetches back to the model as the JSON output: the same window, but without the header line or the `<untrusted_web_content>` wrapper. `AiSdkHarness` does this for you.
If you convert UI messages yourself, pass the same tools to `convertToModelMessages(messages, { tools })`. Without them, the AI SDK sends earlier fetches back to the model as the JSON output: the same window, but without the header line or the `<untrusted_web_content>` wrapper. `ThinkHarness` does this for you.

TanStack AI:

Expand Down
2 changes: 1 addition & 1 deletion docs/agents/search-the-web.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const result = streamText({
});
```

If you convert UI messages yourself, pass the same tools to `convertToModelMessages(messages, { tools })`. Without them, the AI SDK sends earlier search results back to the model as the full JSON response instead of the trimmed text. `AiSdkHarness` does this for you.
If you convert UI messages yourself, pass the same tools to `convertToModelMessages(messages, { tools })`. Without them, the AI SDK sends earlier search results back to the model as the full JSON response instead of the trimmed text. `ThinkHarness` does this for you.

TanStack AI:

Expand Down
35 changes: 18 additions & 17 deletions examples/next/channels/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ Every agent has the same shape:
3. The Worker's **`ChannelGateway`** routes each WebSocket upgrade to the
agent object its route names, which holds the room's conversations.

| Path | Agent | Harness |
| ---------------------------------- | ------------ | ---------------------------------------------------------------- |
| `/channels/pi/<room>[/<conv>]` | `PiAgent` | `PiHarness` from `agents/harness/pi`, behind `piChannelsHarness` |
| `/channels/ai-sdk/<room>[/<conv>]` | `AiSdkAgent` | `AiSdkHarness` from `agents/harness/ai-sdk` |
| Path | Agent | Harness |
| --------------------------------- | ------------ | ---------------------------------------------------------------- |
| `/channels/pi/<room>[/<conv>]` | `PiAgent` | `PiHarness` from `agents/harness/pi`, behind `piChannelsHarness` |
| `/channels/think/<room>[/<conv>]` | `ThinkAgent` | `ThinkHarness` from `agents/harness/think` |

## Files

Expand All @@ -25,14 +25,14 @@ Every agent has the same shape:
client name itself with `?as=` and join any room, which only a demo
should allow: the agent object is the authorization boundary, so whoever
reaches a room may use every conversation in it.
- `src/ai-sdk-agent.ts` is `AiSdkAgent`: `AiSdkHarness` runs each message
with `streamText` on Workers AI. It has a client tool (`getLocation`, run
- `src/think-agent.ts` is `ThinkAgent`: `ThinkHarness` runs each message
with Think's agent loop on Workers AI. It has a client tool (`getLocation`, run
by the participant who asked) and a tool that needs approval (`flipCoin`).
- `src/pi/agent.ts` is `PiAgent`: `PiHarness` runs pi-durable sessions with
a `current_time` tool. See [The pi agent](#the-pi-agent).
- `src/client.tsx` is a browser client built on `WebChannelClient` from
`agents/experimental/channels/web/client`. The URL hash picks the agent and room
(`#ai-sdk/lobby`); the harness picker switches between agents.
(`#think/lobby`); the harness picker switches between agents.

## Run it

Expand All @@ -58,31 +58,32 @@ pnpm tui [room]
```

`src/ai-sdk-tui.ts` runs `@ai-sdk/tui` over `WebChannelChatTransport` from
`agents/experimental/channels/web/ai-sdk`, against the AI SDK agent. The TUI runs
`agents/experimental/channels/web/ai-sdk`, against the Think agent. The TUI runs
`getLocation` itself and asks for approvals with `y` / `n`. Set
`AGENT_ORIGIN` if the dev server is not on `ws://localhost:5173`. The browser
and the TUI can share a room; each is its own participant.

`pnpm tui2 [harness] [room]` runs the Agents SDK terminal client built on Pi
TUI (`npx agents tui <url>`) against either agent, for example
`pnpm tui2 ai-sdk lobby`. It shows turns started from other surfaces, lets you
`pnpm tui2 think lobby`. It shows turns started from other surfaces, lets you
provide client-tool results, and can start, fork, list and switch
conversations (`/new`, `/fork`, `/conversations`, `/switch <id>`). A
conversation is also reachable directly at
`/channels/<harness>/<room>/<conversation>`.

## The pi agent

`PiHarness` keeps pi's own shape. `src/pi/channels-harness.ts`,
`piChannelsHarness(harness, { kv })`, is the harness adapter that puts it
behind the shared harness interface. It only translates shapes: pi entries
become transcript messages, pi events become session events, and pi
submission ids become the caller's operation ids. `Channels.forHarness`
knows nothing about pi:
`PiHarness` keeps pi's own shape. `piChannelsHarness(harness)`,
also from `agents/harness/pi`, is the harness adapter that puts it behind
the shared harness interface: pi events become session events, and pi
submission ids become the caller's operation ids. pi's entries and live
messages become transcript messages and response chunks through the pi
projection (`agents/experimental/channels/projections/pi`).
`Channels.forHarness` knows nothing about pi:

```ts
readonly channels = Channels.forHarness(
piChannelsHarness(this.harness, { kv: this.ctx.storage.kv }),
piChannelsHarness(this.harness),
{ channels: { web: new WebChannel() } }
);
```
Expand All @@ -99,6 +100,6 @@ readonly channels = Channels.forHarness(
| `conversation-reset` | `session.reset(handoff)` |
| `approval-response`, `tool-result` | Rejected: pi-durable has neither yet |

Client tools and approvals therefore work only on the AI SDK agent. Try
Client tools and approvals therefore work only on the Think agent. Try
`pnpm tui2 pi lobby`, ask `What time is it?`, then `/fork`, `/new`, or
`/reset <handoff note>`.
6 changes: 3 additions & 3 deletions examples/next/channels/env.d.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
/* eslint-disable */
// Generated by Wrangler by running `wrangler types --include-runtime=false env.d.ts` (hash: 83849e094f846b2a30145fb06af64f25)
// Generated by Wrangler by running `wrangler types --include-runtime=false env.d.ts` (hash: 02058749c63b605f7cf60262acbc1c20)
interface __BaseEnv_Env {
AI: Ai;
AiSdkAgent: DurableObjectNamespace<import("./src/server").AiSdkAgent>;
ThinkAgent: DurableObjectNamespace<import("./src/server").ThinkAgent>;
PiAgent: DurableObjectNamespace<import("./src/server").PiAgent>;
}
declare namespace Cloudflare {
interface GlobalProps {
mainModule: typeof import("./src/server");
durableNamespaces: "AiSdkAgent" | "PiAgent";
durableNamespaces: "PiAgent" | "ThinkAgent";
}
interface Env extends __BaseEnv_Env {}
}
Expand Down
2 changes: 1 addition & 1 deletion examples/next/channels/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"deploy": "vite build && wrangler deploy",
"types": "wrangler types env.d.ts --include-runtime false",
"tui": "node src/ai-sdk-tui.ts",
"tui2": "sh -c 'tsx ../../../packages/agents/src/cli/index.ts tui \"${AGENT_ORIGIN:-http://localhost:5173}/channels/${1:-ai-sdk}/${2:-default}\" --as \"terminal-$$\"' tui2"
"tui2": "sh -c 'tsx ../../../packages/agents/src/cli/index.ts tui \"${AGENT_ORIGIN:-http://localhost:5173}/channels/${1:-think}/${2:-default}\" --as \"terminal-$$\"' tui2"
},
"dependencies": {
"@cloudflare/kumo": "^2.6.0",
Expand Down
6 changes: 3 additions & 3 deletions examples/next/channels/src/ai-sdk-tui.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Chat with the AI SDK agent from a terminal: pnpm tui [room]
// Chat with the Think agent from a terminal, through the AI SDK TUI: pnpm tui [room]
import { runAgentTUI } from "@ai-sdk/tui";
import { WebChannelChatTransport } from "agents/experimental/channels/web/ai-sdk";
import { WebChannelClient } from "agents/experimental/channels/web/client";
Expand All @@ -7,11 +7,11 @@ const room = process.argv[2] ?? "default";
const origin = process.env.AGENT_ORIGIN ?? "ws://localhost:5173";
const me = `terminal-${crypto.randomUUID().slice(0, 6)}`;
const client = new WebChannelClient(
`${origin}/channels/ai-sdk/${room}?as=${me}`
`${origin}/channels/think/${room}?as=${me}`
);

await runAgentTUI({
title: `Channels: AI SDK agent (${room})`,
title: `Channels: Think agent (${room})`,
reasoning: "full",
tools: "full",
transport: new WebChannelChatTransport(client, {
Expand Down
4 changes: 2 additions & 2 deletions examples/next/channels/src/client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ import { createRoot } from "react-dom/client";

/** The agents this Worker serves, by the route segment that reaches them. */
const harnesses: Record<string, string> = {
"ai-sdk": "AI SDK",
think: "Think",
pi: "pi"
};

// The URL hash picks the agent and room: #<harness>/<room>.
const [hashHarness = "", hashRoom = ""] = location.hash.slice(1).split("/");
const harness = Object.hasOwn(harnesses, hashHarness) ? hashHarness : "ai-sdk";
const harness = Object.hasOwn(harnesses, hashHarness) ? hashHarness : "think";
const room = hashRoom || "default";
const protocol = location.protocol === "https:" ? "wss:" : "ws:";
// Each browser is its own participant, so it runs only its own tool calls.
Expand Down
10 changes: 4 additions & 6 deletions examples/next/channels/src/pi/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,8 @@ import {
import { WebChannel } from "agents/experimental/channels/web";
import { Lifecycle } from "agents/lifecycle";
import { createModels } from "@earendil-works/pi-ai/models";
import { PiHarness } from "agents/harness/pi";
import { PiHarness, piChannelsHarness } from "agents/harness/pi";
import { createAI } from "agents/models/pi-ai";
import { piChannelsHarness } from "./channels-harness";

const MODEL_ID = "@cf/moonshotai/kimi-k2.7-code";

Expand Down Expand Up @@ -63,10 +62,9 @@ export class PiAgent extends DurableObject<Env> {
},
defaults: { model: this.ai(MODEL_ID), thinkingLevel: "low" }
});
readonly channels = Channels.forHarness(
piChannelsHarness(this.harness, { kv: this.ctx.storage.kv }),
{ channels: { web: new WebChannel() } }
);
readonly channels = Channels.forHarness(piChannelsHarness(this.harness), {
channels: { web: new WebChannel() }
});
readonly lifecycle = Lifecycle.install(this)
.use(this.harness)
.use(this.channels.streams)
Expand Down
Loading
Loading