Native Svelte 5 client bindings for the Cloudflare Agents SDK.
svelte-agents mirrors the public client API of Cloudflare's React hooks, but exposes it as Svelte-friendly classes:
useAgent(...) from agents/react becomes new Agent(...).useAgentChat(...) from @cloudflare/ai-chat/react becomes new AgentChat(...).useVoiceAgent(...) from @cloudflare/voice/react becomes new VoiceAgent(...) from svelte-agents/voice.useVoiceInput(...) from @cloudflare/voice/react becomes new VoiceInput(...) from svelte-agents/voice.The intent is React parity, not a new protocol. Your server-side agents, routing, callable methods, state synchronization, AI chat persistence, tool calling, and human-in-the-loop approval flows stay on the Cloudflare Agents SDK. This package only replaces the React client layer with Svelte 5 runes-based classes.
bun add svelte-agents
You still build and deploy the server with the Cloudflare Agents SDK. For example, use Agent, AIChatAgent, routeAgentRequest, getAgentByName, callable, and AI SDK tools from the Cloudflare packages documented in the Agents SDK.
Cloudflare React:
import { useAgent } from 'agents/react';
function Counter() {
const agent = useAgent<{ count: number }>({
agent: 'CounterAgent',
name: 'room-123',
onStateUpdate: (state) => {
console.log('New state:', state);
}
});
return <button onClick={() => agent.stub.increment()}>{agent.name}</button>;
}
Svelte equivalent:
<script lang="ts">
import { Agent } from 'svelte-agents';
const agent = new Agent<{ increment: () => Promise<number> }, { count: number }>({
agent: 'CounterAgent',
name: 'room-123',
onStateUpdate: (state) => {
console.log('New state:', state);
}
});
</script>
<button onclick={() => agent.stub.increment()}>
{agent.name}
</button>
Agent connects to the same /agents/{agent-name}/{instance-name} WebSocket route used by useAgent. Agent class names are converted to kebab-case to match routeAgentRequest, so CounterAgent connects as counter-agent.
These options match Cloudflare's useAgent(options) client API.
| Option | Type | Description |
|---|---|---|
agent |
string |
Required agent class name. Converted to kebab-case for routing. |
name |
string |
Instance name. Defaults to "default". |
host |
string |
Custom host for the Worker. |
path |
string |
Additional path appended to the agent URL. |
basePath |
string |
Full URL path for custom routing, bypassing default /agents/... construction. |
query |
Record<string, string | null> | () => Promise<Record<string, string | null>> |
Query parameters for the connection. |
queryDeps |
unknown[] |
Cache dependencies for async query values. |
cacheTtl |
number |
Query cache TTL in milliseconds. Defaults to five minutes. |
onStateUpdate |
(state, source) => void |
Called when state changes from the server or local client. |
onStateUpdateError |
(error) => void |
Called when a state update error is sent by the agent. |
onMcpUpdate |
(mcpServers) => void |
Called when MCP server state is synchronized. |
onOpen |
(event) => void |
WebSocket open callback. |
onClose |
(event) => void |
WebSocket close callback. |
onError |
(event) => void |
WebSocket error callback. |
onMessage |
(event) => void |
Raw WebSocket message callback. |
onIdentity |
(name, agent) => void |
Called when the server sends identity. |
onIdentityChange |
(oldName, newName, oldAgent, newAgent) => void |
Called when identity changes after reconnect. |
enabled |
boolean |
Enables or disables the connection. |
| Property or method | Description |
|---|---|
agent.agent |
Current agent name, reactive. |
agent.name |
Current instance name, reactive. |
agent.path |
Agent/sub-agent path chain, reactive. |
agent.state |
Latest synchronized state, reactive. |
agent.identified |
Whether server identity has been received, reactive. |
agent.ready |
Promise that resolves when identity is received. |
agent.readyState |
Current WebSocket ready state. |
agent.url |
WebSocket URL. |
agent.bufferedAmount |
WebSocket buffered amount. |
agent.setState(state) |
Push state to the agent. |
agent.call(method, args?, options?) |
Call an agent method over RPC. |
agent.stub |
Proxy for typed method calls, matching Cloudflare's createStubProxy. |
agent.send(data) |
Send a raw WebSocket message. |
agent.close(code?, reason?) |
Close the connection. |
agent.reconnect(code?, reason?) |
Force reconnection. |
agent.addEventListener(...) |
Attach a socket event listener. |
agent.removeEventListener(...) |
Remove a socket event listener. |
agent.getHttpUrl() |
Get the HTTP URL for the same agent route. |
Cloudflare React:
import { useAgent } from 'agents/react';
const agent = useAgent({
agent: 'UserAgent',
basePath: 'user',
onIdentity: (name, agentType) => {
console.log(`Connected to ${agentType} instance: ${name}`);
}
});
Svelte equivalent:
<script lang="ts">
import { Agent } from 'svelte-agents';
const agent = new Agent({
agent: 'UserAgent',
basePath: 'user',
onIdentity: (name, agentType) => {
console.log(`Connected to ${agentType} instance: ${name}`);
}
});
</script>
{#if agent.identified}
<p>Connected to: {agent.name}</p>
{:else}
<p>Connecting...</p>
{/if}
When using basePath, the server chooses the instance and sends identity back to the client. agent.ready resolves when that identity arrives, matching the Cloudflare routing docs.
AgentChat is the Svelte equivalent of Cloudflare's useAgentChat. It wraps @ai-sdk/svelte's Chat class with the same WebSocket transport semantics used by the React hook.
Cloudflare React:
import { useAgent } from 'agents/react';
import { useAgentChat } from '@cloudflare/ai-chat/react';
function Chat() {
const agent = useAgent({ agent: 'ChatAgent' });
const {
messages,
sendMessage,
clearHistory,
addToolOutput,
addToolApprovalResponse,
setMessages,
status
} = useAgentChat({ agent });
return (
<form
onSubmit={(event) => {
event.preventDefault();
const input = event.currentTarget.elements.namedItem('input') as HTMLInputElement;
sendMessage({ text: input.value });
input.value = '';
}}
>
<input name="input" />
<button disabled={status === 'streaming'}>Send</button>
</form>
);
}
Svelte equivalent:
<script lang="ts">
import { Agent, AgentChat } from 'svelte-agents';
const agent = new Agent({ agent: 'ChatAgent' });
const chat = new AgentChat({ agent });
let input = $state('');
function submit() {
if (!input.trim()) return;
chat.sendMessage({ text: input });
input = '';
}
</script>
{#each chat.messages as message (message.id)}
<div>
<strong>{message.role}:</strong>
{#each message.parts as part}
{#if part.type === 'text'}
<span>{part.text}</span>
{/if}
{/each}
</div>
{/each}
<form
onsubmit={(event) => {
event.preventDefault();
submit();
}}
>
<input bind:value={input} placeholder="Type a message..." />
<button disabled={chat.isStreaming}>Send</button>
</form>
These options match Cloudflare's useAgentChat(options) API.
| Option | Default | Description |
|---|---|---|
agent |
Required | An Agent connection. |
messages |
undefined |
Initial messages supplied by the client. |
onToolCall |
undefined |
Handles client-side tool execution. |
autoContinueAfterToolResult |
true |
Continue the conversation after client tool results and approvals. |
resume |
true |
Resume interrupted streams on reconnect. |
body |
undefined |
Object or function merged into every chat request body. |
prepareSendMessagesRequest |
undefined |
Per-request customization for body, headers, credentials, or API URL. |
getInitialMessages |
undefined |
Custom initial message loader. Set to null to skip the HTTP fetch. |
credentials |
undefined |
Fetch credentials for message requests. |
headers |
undefined |
Fetch headers for message requests. |
Deprecated React-parity options are still accepted and warn when used: tools, toolsRequiringConfirmation, experimental_automaticToolResolution, and autoSendAfterAllConfirmationsResolved. Cloudflare's current docs recommend defining tools on the server with the AI SDK tool() function and using needsApproval or onToolCall.
| Property or method | Description |
|---|---|
chat.id |
Underlying chat id. |
chat.messages |
Current UIMessage[], reactive. |
chat.status |
AI SDK status: "idle", "submitted", "streaming", or "error". |
chat.error |
Current chat error. |
chat.lastMessage |
Last message in the conversation. |
chat.isServerStreaming |
true for server-initiated streams. |
chat.isStreaming |
true for either client or server streaming. |
chat.isToolContinuation |
true while continuing after tool output. |
chat.sendMessage(message, options?) |
Send a user message. |
chat.regenerate(options?) |
Regenerate a response. |
chat.resumeStream(options?) |
Resume a stream. |
chat.clearError() |
Clear the current error. |
chat.stop() |
Stop the current stream. |
chat.clearHistory() |
Clear client and server chat history. |
chat.setMessages(messagesOrUpdater) |
Set messages locally and broadcast them to the agent. |
chat.addToolOutput({ toolCallId, output, state?, errorText? }) |
Provide client-side tool output or a custom tool error. |
chat.addToolApprovalResponse({ id, approved }) |
Approve or reject a tool requiring approval. |
chat.addToolResult(...) |
Deprecated alias for addToolOutput. |
chat.destroy() |
Remove listeners and stop Svelte effects. Call this for manually managed lifetimes outside components. |
Cloudflare React:
const { messages, addToolApprovalResponse } = useAgentChat({ agent });
messages
.flatMap((message) => message.parts)
.filter((part) => part.type === 'tool' && part.state === 'approval-required')
.map((part) => (
<button onClick={() => addToolApprovalResponse({ id: part.toolCallId, approved: true })}>
Approve
</button>
));
Svelte equivalent:
<script lang="ts">
import { getToolCallId, getToolPartState } from 'svelte-agents';
</script>
{#each chat.messages
.flatMap((message) => message.parts)
.filter((part) => part.type === 'tool' && getToolPartState(part) === 'waiting-approval') as part}
{@const toolCallId = getToolCallId(part)}
<button onclick={() => chat.addToolApprovalResponse({ id: toolCallId, approved: true })}>
Approve
</button>
{/each}
Client-side tools use the same onToolCall pattern as the React hook:
<script lang="ts">
import { Agent, AgentChat } from 'svelte-agents';
const agent = new Agent({ agent: 'ChatAgent' });
const chat = new AgentChat({
agent,
onToolCall: async ({ toolCall, addToolOutput }) => {
if (toolCall.toolName === 'getLocation') {
const position = await new Promise<GeolocationPosition>((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject);
});
addToolOutput({
toolCallId: toolCall.toolCallId,
output: {
lat: position.coords.latitude,
lng: position.coords.longitude
}
});
}
}
});
</script>
Voice bindings live on the svelte-agents/voice subpath so the root package can stay focused on text agents and chat:
import { VoiceAgent, VoiceInput } from 'svelte-agents/voice';
Server-side voice agents still come from Cloudflare's voice package:
import { Agent } from 'agents';
import { withVoice, WorkersAIFluxSTT, WorkersAITTS } from '@cloudflare/voice';
VoiceAgent is the Svelte equivalent of Cloudflare's useVoiceAgent. It wraps VoiceClient for withVoice agents and manages connection state, microphone capture, playback, silence detection, interrupt detection, transcripts, metrics, and custom app messages.
Cloudflare React:
import { useVoiceAgent } from '@cloudflare/voice/react';
function VoiceUI() {
const { status, connected, audioLevel, isMuted, startCall, endCall, toggleMute } = useVoiceAgent({
agent: 'MyAgent',
name: 'default',
host: window.location.host
});
return (
<>
<p>Status: {status}</p>
<p>Connected: {String(connected)}</p>
<p>Audio level: {audioLevel}</p>
<button onClick={status === 'idle' ? startCall : endCall}>
{status === 'idle' ? 'Start call' : 'End call'}
</button>
<button onClick={toggleMute}>{isMuted ? 'Unmute' : 'Mute'}</button>
</>
);
}
Svelte equivalent:
<script lang="ts">
import { VoiceAgent } from 'svelte-agents/voice';
const voice = new VoiceAgent({
agent: 'MyAgent',
name: 'default',
host: window.location.host
});
</script>
<p>Status: {voice.status}</p>
<p>Connected: {String(voice.connected)}</p>
<p>Audio level: {voice.audioLevel}</p>
<button onclick={voice.status === 'idle' ? voice.startCall : voice.endCall}>
{voice.status === 'idle' ? 'Start call' : 'End call'}
</button>
<button onclick={voice.toggleMute}>
{voice.isMuted ? 'Unmute' : 'Mute'}
</button>
| Option | Default | Description |
|---|---|---|
agent |
Required | Agent class name. |
name |
"default" |
Instance name. |
host |
window.location.host |
Worker host. |
query |
undefined |
Query parameters appended to the WebSocket URL. |
transport |
undefined |
Custom voice transport. Defaults to WebSocket via PartySocket. |
audioInput |
undefined |
Custom audio input source for WebRTC, SFU, or other routing. |
preferredFormat |
undefined |
Preferred server audio format hint. |
silenceThreshold |
0.04 |
RMS below this is silence. |
silenceDurationMs |
500 |
Silence duration before end-of-speech. |
interruptThreshold |
0.05 |
RMS threshold for detecting speech during playback. |
interruptChunks |
2 |
Consecutive high-RMS chunks needed to interrupt playback. |
maxTranscriptMessages |
200 |
Maximum transcript messages kept by VoiceClient. |
onReconnect |
undefined |
Called when voice.update(...) changes the connection key. |
| Property or method | Description |
|---|---|
voice.status |
"idle", "listening", "thinking", or "speaking". |
voice.transcript |
TranscriptMessage[] conversation history. |
voice.interimTranscript |
Current partial transcript, or null. |
voice.metrics |
Latest pipeline timing metrics, or null. |
voice.audioLevel |
Current microphone RMS level from 0 to 1. |
voice.isMuted |
Whether microphone audio is muted. |
voice.connected |
Whether the voice transport is connected. |
voice.error |
Current error message, or null. |
voice.lastCustomMessage |
Last non-voice-protocol message from the server. |
voice.startCall() |
Request microphone access and begin streaming audio. |
voice.endCall() |
End the call and release microphone resources. |
voice.toggleMute() |
Toggle microphone mute. |
voice.sendText(text) |
Send text directly, bypassing STT. |
voice.sendJSON(data) |
Send arbitrary JSON app messages to the agent. |
voice.update(options) |
Reconnect when connection identity or tuning options change. |
voice.destroy() |
Remove listeners and disconnect the underlying VoiceClient. |
VoiceInput is the Svelte equivalent of Cloudflare's useVoiceInput. It is optimized for dictation and speech-to-text UI: it accumulates user transcripts into a single string and ignores assistant/TTS responses.
Cloudflare React:
import { useVoiceInput } from '@cloudflare/voice/react';
function Dictation() {
const { transcript, interimTranscript, isListening, start, stop, clear } = useVoiceInput({
agent: 'DictationAgent'
});
return (
<>
<textarea value={transcript + (interimTranscript ? ' ' + interimTranscript : '')} readOnly />
<button onClick={isListening ? stop : start}>{isListening ? 'Stop' : 'Dictate'}</button>
<button onClick={clear}>Clear</button>
</>
);
}
Svelte equivalent:
<script lang="ts">
import { VoiceInput } from 'svelte-agents/voice';
const voice = new VoiceInput({ agent: 'DictationAgent' });
</script>
<textarea
value={voice.transcript + (voice.interimTranscript ? ` ${voice.interimTranscript}` : '')}
readonly
/>
<button onclick={voice.isListening ? voice.stop : voice.start}>
{voice.isListening ? 'Stop' : 'Dictate'}
</button>
<button onclick={voice.clear}>Clear</button>
| Option | Default | Description |
|---|---|---|
agent |
Required | Agent class name. |
name |
"default" |
Instance name. |
host |
window.location.host |
Worker host. |
silenceThreshold |
0.04 |
RMS below this is silence. |
silenceDurationMs |
500 |
Silence duration before end-of-speech. |
| Property or method | Description |
|---|---|
voice.transcript |
Accumulated final user transcript text. |
voice.interimTranscript |
Current partial transcript, or null. |
voice.isListening |
Whether the mic is actively listening. |
voice.audioLevel |
Current microphone RMS level from 0 to 1. |
voice.isMuted |
Whether microphone audio is muted. |
voice.error |
Current error message, or null. |
voice.start() |
Request microphone access and begin streaming audio. |
voice.stop() |
Stop listening and release microphone resources. |
voice.toggleMute() |
Toggle microphone mute. |
voice.clear() |
Clear the accumulated transcript. |
voice.update(options) |
Reconnect when connection identity or tuning options change. |
voice.destroy() |
Remove listeners and disconnect the underlying VoiceClient. |
import {
Agent,
AgentChat,
getAgentMessages,
getToolApproval,
getToolCallId,
getToolInput,
getToolOutput,
getToolPartState
} from 'svelte-agents';
| Export | Description |
|---|---|
Agent |
Svelte class equivalent of useAgent. |
AgentChat |
Svelte class equivalent of useAgentChat. |
getAgentMessages(...) |
Fetch persisted chat messages from an agent. |
getToolPartState(part) |
Normalize AI SDK tool part states for UI rendering. |
getToolCallId(part) |
Read a tool call id from a tool part. |
getToolInput(part) |
Read tool input from a tool part. |
getToolOutput(part) |
Read tool output from a tool part. |
getToolApproval(part) |
Read tool approval metadata from a tool part. |
extractClientToolSchemas(...) |
Deprecated React-parity helper. |
detectToolsRequiringConfirmation(...) |
Deprecated React-parity helper. |
routeAgentRequest, AIChatAgent, callable methods, routing, and Workers deployment.Agent and AgentChat use Svelte runes internally, so properties like agent.state, agent.name, chat.messages, and chat.isStreaming update reactively in Svelte templates.