Skip to content

feat(harness): add an AI SDK harness - #2434

Merged
cjol merged 1 commit into
channels/05-webfrom
channels/06-ai-sdk-connectors
Oct 2, 2026
Merged

cjol merged 1 commit into
channels/05-webfrom
channels/06-ai-sdk-connectors

Conversation

@cjol

@cjol cjol commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

This PR adds agents/harness/ai-sdk: AiSdkHarness, which serves an AI SDK model through Channels, and the AI SDK conversions it is built on.

Why

  • Channels serves a harness. An agent written against the AI SDK has no harness of its own, so without one every agent would rebuild sessions, a transcript, a turn queue and recovery around streamText.
  • An AI SDK agent speaks UIMessage and UIMessageChunk. Channels speaks TranscriptMessage and ResponseChunk. The conversions, including tool states and approvals, live with the harness that needs them.
  • The harness is opt-in and knows nothing about Channels setup. It implements the shared interface, as the pi adapter does, and sits next to agents/harness/pi.

Public API Surface

Additive, from agents/harness/ai-sdk:

Symbol Kind Notes
AiSdkHarness, AiSdkHarnessOptions class, type Lifecycle capability: model, tools, system, stopWhen, heartbeatMs
toResponseChunks function Converts an AI SDK stream to response chunks
toTranscriptMessage, toUIMessage functions Message conversions in each direction
awaitsInput, answerToolCall, ToolCallAnswer functions, type Approvals and client tool results
createSendMessageTool, CreateSendMessageToolOptions function, type AI SDK tool that sends to a surface through the gateway
AiSdkConversionOptions type Conversion options

Code Changes

  • harness/ai-sdk/harness.ts keeps sessions, transcripts and an operation queue in the object's storage. Each operation runs one streamText call over the session's transcript, and a Lifecycle wake job runs the queue again after an eviction. A client tool takes its result only from the participant who started the turn, while approvals come from anyone. A steer queues like a follow-up.
  • harness/ai-sdk/turns.ts holds the conversions, and send-message-tool.ts the send tool.
  • New entry point in package.json and scripts/build.ts, and a minor changeset for agents.
  • ai-sdk-harness.test.ts runs the harness behind Channels and the Web Channel with a mock model.

@changeset-bot

changeset-bot Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: fec4998

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
agents Minor
@cloudflare/agent-think Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@cjol
cjol added this pull request to stack #2440 October 1, 2026 13:56
devin-ai-integration[bot]

This comment was marked as resolved.

@agent-think

agent-think Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

🟢 agents import sizes: 1 entry point changed, no growth

Entry point Exports Largest gzip change Size now
🆕 agents/harness/ai-sdk 7 new — 157.4 KiB
Changed exports (7)
Import Gzip change Size now
🆕 agents/harness/ai-sdk#AiSdkHarness — 157.4 KiB
🆕 agents/harness/ai-sdk#toResponseChunks — 112.8 KiB
🆕 agents/harness/ai-sdk#toTranscriptMessage — 112.7 KiB
🆕 agents/harness/ai-sdk#answerToolCall — 112.4 KiB
🆕 agents/harness/ai-sdk#toUIMessage — 112.3 KiB
🆕 agents/harness/ai-sdk#awaitsInput — 112.2 KiB
🆕 agents/harness/ai-sdk#createSendMessageTool — 112.2 KiB
How this works

Each runtime export is bundled on its own, minified, and gzipped. Changes smaller than 100 B, or smaller than 1% and 1 KiB, are ignored. Growth over 10% or 5 KiB is marked 🔴. This report is informational and does not fail CI. The workflow artifact contains every measurement.

Compared 1b17acd1 → fec4998a · workflow run · reported by agent-think[bot]

@cjol
cjol force-pushed the channels/06-ai-sdk-connectors branch 2 times, most recently from c1b3768 to e308e8c Compare October 1, 2026 14:27
@pkg-pr-new

pkg-pr-new Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

agents

npm i https://pkg.pr.new/agents@2434

@cloudflare/ai-chat

npm i https://pkg.pr.new/@cloudflare/ai-chat@2434

@cloudflare/codemode

npm i https://pkg.pr.new/@cloudflare/codemode@2434

hono-agents

npm i https://pkg.pr.new/hono-agents@2434

@cloudflare/shell

npm i https://pkg.pr.new/@cloudflare/shell@2434

@cloudflare/think

npm i https://pkg.pr.new/@cloudflare/think@2434

@cloudflare/voice

npm i https://pkg.pr.new/@cloudflare/voice@2434

@cloudflare/worker-bundler

npm i https://pkg.pr.new/@cloudflare/worker-bundler@2434

commit: fec4998

@cjol
cjol force-pushed the channels/06-ai-sdk-connectors branch from e308e8c to af3f181 Compare October 2, 2026 10:13
@cjol
cjol force-pushed the channels/06-ai-sdk-connectors branch from af3f181 to 6ab1014 Compare October 2, 2026 11:08
@cjol
cjol removed this pull request from stack #2440 October 2, 2026 17:37
@cjol
cjol force-pushed the channels/06-ai-sdk-connectors branch from 6ab1014 to 245a7b9 Compare October 2, 2026 17:37
@cjol cjol changed the title feat(channels): add AI SDK agent connectors feat(harness): add an AI SDK harness Oct 2, 2026
@cjol
cjol added this pull request to stack #2460 October 2, 2026 17:39

@devin-ai-integration devin-ai-integration Bot left a comment

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.

Devin Review found 4 new potential issues.

Devin Review

Comment on lines +278 to +280
for (const [key] of this.#kv.list({ prefix: messagePrefix(session) })) {
this.#kv.delete(key);
}

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.

🔴 Reset blocks replay of old messages

After reset, resending an earlier message fails because #operation still finds its settled operation ID. Channels clears its event cache for replay, but the transcript cannot be rebuilt.

Learn more

Reset removes transcript entries but leaves operation records and their deduplication keys in durable storage. Channels explicitly clears its own seen-event cache on reset so a previously accepted event can be resent to rebuild the conversation in #reset. When that event uses its original ID, #submit finds the old operation and rejects it without placing a new message.

Example: A message with event ID e1 completes. The user resets the conversation and resends e1; Channels accepts it again, but the harness returns accepted: false, leaving the new transcript empty.

Recommended fix: Clear or scope settled operation records and sequence/index state to the reset generation, while retaining any deduplication required for operations still being aborted. Verify that replaying an old event ID after reset places its message and starts a new run.

Devin Review


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

Comment on lines +467 to +474
const holder = messages.find((m) =>
m.parts.some(
(p) => "toolCallId" in p && p.toolCallId === answer.toolCallId
)
);
const owner = holder?.metadata?.owner;
const from = answer.from?.participantId;
if (owner !== undefined && from !== undefined && owner !== from) {

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.

🟡 Reused tool IDs reject valid answers

When a later client tool reuses a tool ID, #answer checks the first historical call's owner. A different participant's valid result gets not_owner, leaving the current call unanswered.

Learn more

Tool call IDs come from the model and are not namespaced by session turn. The ownership lookup searches all saved messages in order, without checking whether the call still awaits a result. By contrast, answerToolCall selects a call by both ID and eligible state. When an old completed call and a current pending call share an ID, their owners can differ, and this ownership check rejects the current owner's answer.

Example: Alice answers tool call c1 in the first turn. Bob starts a later turn whose model also emits c1. Bob's answer sees Alice's completed call first and is refused as not_owner.

Recommended fix: Locate the same pending tool part that answerToolCall will update, then check that message's owner. Prefer one shared matching helper so the ownership check and update cannot select different calls.

Devin Review


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

Comment on lines +255 to +256
waiters.add(done);
signal?.addEventListener("abort", aborted, { once: true });

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.

🟡 Pre-aborted waits remain pending

When wait receives an already aborted signal, it registers a listener after the abort event. The wait cannot cancel until the operation settles, leaving callers blocked.

Learn more

An AbortSignal dispatches its abort event only once. If wait(operationId, signal) starts with a pending operation and an already aborted signal, the newly added listener never runs; only operation settlement resolves the promise. The shared wait contract accepts a signal for cancellation.

Example: An operation waits on a slow model and a caller passes AbortSignal.abort() to wait. Instead of rejecting immediately, its promise stays pending until the model run ends.

Recommended fix: Check signal.aborted before registering the waiter and reject with signal.reason. Also remove the waiter and its map entry when an active signal aborts.

Devin Review


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

Comment on lines +170 to +174
"./harness/ai-sdk": {
"types": "./dist/harness/ai-sdk/index.d.ts",
"import": "./dist/harness/ai-sdk/index.js",
"require": "./dist/harness/ai-sdk/index.js"
},

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.

🔍 Advertised import path differs from exported API

The PR description advertises agents/channels/ai-sdk, but the package exports agents/harness/ai-sdk and a hosted harness. Align the public API documentation with the intended release.

Devin Review


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

@cjol
cjol force-pushed the channels/06-ai-sdk-connectors branch from 245a7b9 to 22f8aa5 Compare October 2, 2026 18:20
This PR adds `agents/harness/ai-sdk`: `AiSdkHarness`, which serves an AI SDK model through Channels, and the AI SDK conversions it is built on.

## Why

- Channels serves a harness. An agent written against the AI SDK has no harness of its own, so without one every agent would rebuild sessions, a transcript, a turn queue and recovery around `streamText`.
- An AI SDK agent speaks `UIMessage` and `UIMessageChunk`. Channels speaks `TranscriptMessage` and `ResponseChunk`. The conversions, including tool states and approvals, live with the harness that needs them.
- The harness is opt-in and knows nothing about Channels setup. It implements the shared interface, as the pi adapter does, and sits next to `agents/harness/pi`.

## Public API Surface

Additive, from `agents/harness/ai-sdk`:

| Symbol | Kind | Notes |
| --- | --- | --- |
| `AiSdkHarness`, `AiSdkHarnessOptions` | class, type | Lifecycle capability: `model`, `tools`, `system`, `stopWhen`, `heartbeatMs` |
| `toResponseChunks` | function | Converts an AI SDK stream to response chunks |
| `toTranscriptMessage`, `toUIMessage` | functions | Message conversions in each direction |
| `awaitsInput`, `answerToolCall`, `ToolCallAnswer` | functions, type | Approvals and client tool results |
| `createSendMessageTool`, `CreateSendMessageToolOptions` | function, type | AI SDK tool that sends to a surface through the gateway |
| `AiSdkConversionOptions` | type | Conversion options |

## Code Changes

- `harness/ai-sdk/harness.ts` keeps sessions, transcripts and an operation queue in the object's storage. Each operation runs one `streamText` call over the session's transcript, and a Lifecycle wake job runs the queue again after an eviction. A client tool takes its result only from the participant who started the turn, while approvals come from anyone. A steer queues like a follow-up.
- `harness/ai-sdk/turns.ts` holds the conversions, and `send-message-tool.ts` the send tool.
- New entry point in `package.json` and `scripts/build.ts`, and a minor changeset for `agents`.
- `ai-sdk-harness.test.ts` runs the harness behind Channels and the Web Channel with a mock model.
@cjol
cjol force-pushed the channels/06-ai-sdk-connectors branch from 22f8aa5 to fec4998 Compare October 2, 2026 18:53
@cjol
cjol merged commit 0ebeae5 into main Oct 2, 2026
16 of 30 checks passed
@cjol
cjol deleted the channels/06-ai-sdk-connectors branch October 2, 2026 19:20
@github-actions github-actions Bot mentioned this pull request Oct 2, 2026
brandhaug added a commit to brandhaug/b2b-saas-starter that referenced this pull request Oct 10, 2026
## pnpm-workspace.yaml (default)

## Dependency Updates

| Package | From | To | Type |
| --- | --- | --- | --- |
| `agents` | 0.26.0 | 0.27.0 | minor |

## Release Notes

<details>
<summary><b>agents</b> (0.26.0 → 0.27.0)</summary>

This release adds four new experimental harnesses and a `web_search`
tool, and reorganizes the experimental Channels API.

- **New experimental harnesses:** `AiSdkHarness`, `ThinkHarness`,
`ContainerHarness` (which runs Claude Code or Codex in a Container) and
`OpenCodeHarness`, all with the same shape as `PiHarness`.
- **New `agents/websearch`:** a `web_search` tool over Cloudflare's Web
Search API for pi, the AI SDK and TanStack AI.
- **Channels moved to `agents/experimental/channels`:** Channels is
being rebuilt around conversations and turns, and the old
`agents/channels` entry point has been removed. The new API includes
`ChannelGateway`, the Web Channel and its client, an AI SDK chat
transport, and a new `npx agents tui` terminal client.
- **Fixes:** Streams, Tasks, `useAgentChat` reconnects,
`WebChannelClient`, MCP credential cleanup on `removeServer()`, and x402
with MCP SDK v2.

### Minor Changes

- [#2434](cloudflare/agents#2434)
[`0ebeae5`](cloudflare/agents@0ebeae5)
Thanks [@<!---->cjol](https://github.com/cjol)! - Add `AiSdkHarness`
from the new `agents/harness/ai-sdk` entry point, which runs each
message with `streamText` and keeps sessions and transcripts in the
Durable Object, so `Channels.forHarness` can serve an AI SDK model. The
same entry point exports the AI SDK message conversions it is built on
and `createSendMessageTool`, an AI SDK tool that sends a message to a
surface through the gateway.

- [#2431](cloudflare/agents#2431)
[`6393265`](cloudflare/agents@6393265)
Thanks [@<!---->cjol](https://github.com/cjol)! - Add `Channels` to
`agents/experimental/channels`: a Lifecycle capability that serves an
agent harness's sessions as conversations to every surface that joins
them, with durable, resumable responses on `Streams`. Connect a harness
with `Channels.forHarness(harness, { channel

…[full
notes](https://github.com/cloudflare/agents/releases/tag/agents%400.27.0)

</details>

---
*This PR was auto-generated by
[catalog-update-action](https://github.com/brandhaug/catalog-update-action).*

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant