Add Kitesurf support to Browser Tools - #2098
Conversation
🦋 Changeset detectedLatest commit: cc89773 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
@cloudflare/ai-chat
@cloudflare/codemode
hono-agents
@cloudflare/shell
@cloudflare/think
@cloudflare/voice
@cloudflare/worker-bundler
commit: |
|
Pushed cc89773 for the two Devin Review findings (on top of c29c3c7):
|
| ...mcpTools, | ||
|
|
||
| // Browser tools: durable CDP browser_execute + stateless Quick Actions | ||
| // One-shot Kitesurf CDP browser_execute tool |
There was a problem hiding this comment.
🔴 Screenshots re-enter the chat history at full size on the next message
The chat history is rebuilt for the model without telling it how each browser result should be summarized (convertToModelMessages(this.messages) at examples/ai-chat/src/server.ts:105), so a captured screenshot is resent in full on the following turn instead of the short summary.
Impact: After a screenshot, the very next user message ships the entire image as text to a text-only model, blowing up context and cost and likely failing the request.
Why the compact summary is bypassed on later turns
createBrowserRuntime attaches toModelOutput to browser_execute (packages/agents/src/browser/ai.ts:392-393) so the model sees only "Screenshot captured successfully …". That hook is applied by streamText for results produced during the current run, and by convertToModelMessages only when the tool set is passed in options — see the in-repo convention at packages/think/src/think.ts:5327-5330 (convertToModelMessages(truncated, { tools, ignoreIncompleteToolCalls: true })).
The example calls convertToModelMessages(this.messages) with no tools, so persisted tool parts are converted with their raw output. Because transformBrowserResult deliberately leaves canonical screenshot results untruncated (packages/agents/src/browser/ai.ts:250-257), the persisted part contains the complete base64 payload. pruneMessages({ toolCalls: "before-last-2-messages" }) keeps the most recent assistant turn's tool results, so the screenshot is included verbatim in the next request.
Prompt for agents
In examples/ai-chat/src/server.ts, onChatMessage builds model messages with `convertToModelMessages(this.messages)` and no tool set. The browser_execute tool created by createBrowserTools defines `toModelOutput` (packages/agents/src/browser/ai.ts) so that a canonical screenshot result is replaced by a short text summary, while the raw base64 stays in the persisted UI message for inline rendering. Because convertToModelMessages only applies toModelOutput when the tools are passed in its options (see packages/think/src/think.ts for the in-repo convention), every follow-up turn re-serializes the full base64 screenshot into the model request. Fix by constructing the full tool set once and passing it to convertToModelMessages (e.g. `convertToModelMessages(this.messages, { tools, ignoreIncompleteToolCalls: true })`) so persisted browser results are summarized consistently.
Was this helpful? React with 👍 or 👎 to provide feedback.
| connectors: [connector], | ||
| name: options.name ?? "browser", | ||
| transformResult: truncateResult | ||
| transformResult: transformBrowserResult |
There was a problem hiding this comment.
🟡 Browser screenshots are sent to the model unbounded through the TanStack wrapper
Screenshot results are now exempted from the size cap that used to bound every browser result (transformResult: transformBrowserResult at packages/agents/src/browser/ai.ts:378), so callers that have no way to substitute a short summary hand the whole image to the model.
Impact: A TanStack AI agent that takes a screenshot pushes up to a megabyte of image text into the model request, wasting context and potentially failing the call.
Only the AI SDK path has the compensating summary hook
transformBrowserResult (packages/agents/src/browser/ai.ts:250-257) returns canonical { type: "browser_screenshot", mediaType, data } results untouched instead of running them through truncateResult, relying on the AI SDK-only toModelOutput hook added at packages/agents/src/browser/ai.ts:390-394 to keep the model context small.
packages/agents/src/browser/tanstack-ai.ts:44-69 builds its ServerTool from createBrowserRuntime(...).tools.browser_execute but only forwards description, inputSchema and execute; the returned ProxyToolOutput (including result) goes straight back to the model. The connector hint injected for both paths (packages/agents/src/browser/ai.ts:383-387) explicitly instructs the model to return that screenshot shape, so this path is reachable in practice. Before this change truncateResult capped the payload.
Prompt for agents
transformBrowserResult in packages/agents/src/browser/ai.ts now bypasses truncateResult for canonical browser_screenshot results, on the assumption that the tool's toModelOutput hook will give the model a compact summary. That hook only exists on the AI SDK tool object. packages/agents/src/browser/tanstack-ai.ts wraps the same runtime but forwards only description/inputSchema/execute, so a TanStack consumer returns the untruncated base64 result directly to the model. Consider applying the same summarization in the TanStack wrapper (reuse browserExecuteModelOutput / a shared exported helper on the returned ProxyToolOutput), or making the screenshot passthrough opt-in for consumers that can render the image.
Was this helpful? React with 👍 or 👎 to provide feedback.
This PR adds connection-scoped Kitesurf support to Agents Browser Tools and uses it in the
ai-chatexample. Fixes #2095.Why
/v1/devtools/browser?browser=kitesurf, where the WebSocket itself owns the browser lifetime.quickAction()RPC cannot currently carry an engine selector. Passingbrowserin its options is rejected as part of the action body.Public API Surface
ConnectBrowserOptions.browser"kitesurf"engine for direct CDP acquisition.BrowserConnectorSessionOptions.browsercreateBrowserTools()andcreateBrowserRuntime().Selecting Kitesurf defaults Quick Actions off because the binding cannot select that engine. Callers can explicitly request Quick Actions to create a mixed toolset where CDP uses Kitesurf and Quick Actions use Chromium.
Architectural Changes
Code Changes
browser-run.tsacquires Kitesurf directly over WebSocket and rejects incompatible Browser Run options before making a request.BrowserConnectorselects the Kitesurf acquisition path, narrows its model-facing tools and instructions, and fails explicitly when a connection can no longer resume.browser_executepreserves canonical screenshot results for AI SDK UIs whiletoModelOutputsends the model only a compact attachment summary.codemode.search().examples/ai-chatdemonstrates natural-language Kitesurf navigation and inline screenshot rendering with a remote Browser Run binding.