Skip to content

feat(agents): add experimental outbound channels - #2086

Open
cjol wants to merge 1 commit into
mainfrom
investigate/think-channels-package-prototype
Open

feat(agents): add experimental outbound channels#2086
cjol wants to merge 1 commit into
mainfrom
investigate/think-channels-package-prototype

Conversation

@cjol

@cjol cjol commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

This PR adds an experimental transport-neutral outbound Channels API at agents/experimental/channels, with an Email Service adapter and an AI SDK tool bridge. Think is an integration consumer, but it does not appear in the Channels contract.

Why

  • Outbound actions are currently built as transport-specific tools, so changing from email to another delivery route also changes the model-facing contract.
  • A small canonical message shape, optional title plus Markdown content, gives adapters one semantic payload to project into transport-specific fields.
  • Direct delivery can fail after a side effect may have occurred. Returning delivered, safely retryable or permanent failed, and uncertain outcomes lets callers avoid blindly duplicating messages.
  • A standalone package was considered, but an experimental agents entry point keeps the first slice small while the abstraction is validated.
  • The first slice intentionally tests only whether a canonical payload and configured-route tool are useful before introducing a broader channel lifecycle.

Not Included

This is deliberately narrower than the broader Channels direction discussed previously. At this stage it is a tool wrapper around one configured outbound transport.

  • Email is the only included transport adapter. There are no chat, SMS, voice, or other channel implementations.
  • Delivery is immediate and in-process. There is no durable submission record, queue, retry worker, or recovery mechanism.
  • There is no inbound processing, identity resolution, thread routing, or request-to-agent continuity.
  • Normal assistant output is not delivered through a Channel. The model must explicitly call a tool created with createChannelTool() for anything to be sent.

Public API Surface

All additions are exported from agents/experimental/channels.

Symbol Kind Notes
ChannelMessage Type Canonical optional title and Markdown content
DeliveryFailure Type Model-visible failure code and message
DeliveryResult Type Delivered, failed, or uncertain direct-delivery outcome
Channel Interface Configured outbound route with deliver()
CreateChannelToolOptions Type Caller-owned AI SDK description, examples, metadata, and approval policy
createChannelTool() Function Adapts a configured Channel to an AI SDK tool
RenderedEmailMarkdown Type Text and HTML projection returned by an email renderer
EmailChannelOptions Type Destination-bound Email Service channel configuration
email() Function Creates an Email Service-backed Channel

The existing EmailSendBinding export remains available under the same name and now aliases Cloudflare's platform SendEmail type. The existing SendEmailOptions shape is preserved.

Architectural Changes

AI SDK tool
    |
createChannelTool()
    |
configured Channel
    |
transport adapter
    |
Email Service binding
  • The generic channel contract owns the canonical message and delivery semantics.
  • Each adapter binds addressing at construction time and owns transport projection and error classification.
  • Tool names and policy stay with the caller's ToolSet, so the Channels module does not prescribe product behavior.

Code Changes

  • Adds the experimental Channels entry point, generic contract, and AI SDK tool adapter.
  • Adds a destination-bound email adapter that maps title to subject, renders Markdown as text by default, supports caller-provided text and HTML rendering, and conservatively classifies ambiguous failures as uncertain.
  • Moves Agent.sendEmail() message construction into an internal email helper while preserving Agent routing headers, signing, observability, error handling, and the existing public method.
  • Wires the new entry point into the package build and export map and includes a minor changeset for agents.

@changeset-bot

changeset-bot Bot commented Aug 10, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 75b28e1

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

@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 1 potential issue.

View 1 additional finding in Devin Review.

Open in Devin Review

Comment on lines +113 to +117
async deliver(message) {
const rendered = renderMarkdown(message.markdown);
if (rendered.text === undefined && rendered.html === undefined) {
throw new Error("renderMarkdown must return text or html content");
}

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.

🟡 Email channel throws an error instead of reporting a failed delivery when the message body comes out empty

The email route raises an exception (throw new Error at packages/agents/src/experimental/channels/email.ts:116) outside the protected send path when the message body renders to nothing, so callers and models get a crash instead of the promised delivery outcome.
Impact: A tool call that should report a clean "failed" outcome instead blows up the surrounding agent turn.

Contract mismatch between deliver() and its documented DeliveryResult outcomes

Channel.deliver is declared to always resolve to a DeliveryResult (packages/agents/src/experimental/channels/channel.ts:41-43), and createChannelTool wires deliver straight into the AI SDK tool's execute (packages/agents/src/experimental/channels/channel.ts:78). In email.ts the guard on the rendered projection, and any exception thrown by a caller-supplied renderMarkdown, happen before the try block at packages/agents/src/experimental/channels/email.ts:119, so they reject the promise rather than returning { status: "failed", retryable: false, ... }. Only binding errors are classified. Moving the render call and the emptiness check inside the try (or returning a failed result) would keep the contract consistent.

Suggested change
async deliver(message) {
const rendered = renderMarkdown(message.markdown);
if (rendered.text === undefined && rendered.html === undefined) {
throw new Error("renderMarkdown must return text or html content");
}
async deliver(message) {
let rendered: RenderedEmailMarkdown;
try {
rendered = renderMarkdown(message.markdown);
if (rendered.text === undefined && rendered.html === undefined) {
throw new Error("renderMarkdown must return text or html content");
}
} catch (error) {
return {
status: "failed",
retryable: false,
error: emailFailure(error)
};
}
Open in Devin Review

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

@pkg-pr-new

pkg-pr-new Bot commented Aug 10, 2026

Copy link
Copy Markdown

Open in StackBlitz

agents

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

@cloudflare/ai-chat

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

@cloudflare/codemode

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

create-think

npm i https://pkg.pr.new/create-think@2086

hono-agents

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

@cloudflare/shell

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

@cloudflare/think

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

@cloudflare/voice

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

@cloudflare/worker-bundler

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

commit: 75b28e1

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