Repository navigation
feat(harness): add an AI SDK harness - #2434
Conversation
🦋 Changeset detectedLatest commit: fec4998 The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
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 |
🟢 agents import sizes: 1 entry point changed, no growth
Changed exports (7)
How this worksEach 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 |
c1b3768 to
e308e8c
Compare
agents
@cloudflare/ai-chat
@cloudflare/codemode
hono-agents
@cloudflare/shell
@cloudflare/think
@cloudflare/voice
@cloudflare/worker-bundler
commit: |
e308e8c to
af3f181
Compare
af3f181 to
6ab1014
Compare
6ab1014 to
245a7b9
Compare
| for (const [key] of this.#kv.list({ prefix: messagePrefix(session) })) { | ||
| this.#kv.delete(key); | ||
| } |
There was a problem hiding this comment.
🔴 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
| 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) { |
There was a problem hiding this comment.
🟡 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
| waiters.add(done); | ||
| signal?.addEventListener("abort", aborted, { once: true }); |
There was a problem hiding this comment.
🟡 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
| "./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" | ||
| }, |
There was a problem hiding this comment.
245a7b9 to
22f8aa5
Compare
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.
22f8aa5 to
fec4998
Compare
## 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>
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
streamText.UIMessageandUIMessageChunk. Channels speaksTranscriptMessageandResponseChunk. The conversions, including tool states and approvals, live with the harness that needs them.agents/harness/pi.Public API Surface
Additive, from
agents/harness/ai-sdk:AiSdkHarness,AiSdkHarnessOptionsmodel,tools,system,stopWhen,heartbeatMstoResponseChunkstoTranscriptMessage,toUIMessageawaitsInput,answerToolCall,ToolCallAnswercreateSendMessageTool,CreateSendMessageToolOptionsAiSdkConversionOptionsCode Changes
harness/ai-sdk/harness.tskeeps sessions, transcripts and an operation queue in the object's storage. Each operation runs onestreamTextcall 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.tsholds the conversions, andsend-message-tool.tsthe send tool.package.jsonandscripts/build.ts, and a minor changeset foragents.ai-sdk-harness.test.tsruns the harness behind Channels and the Web Channel with a mock model.