agents-vue 0.2.1 β†’ 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,39 @@
1
1
  # Changelog
2
2
 
3
3
 
4
+ ## v0.4.0
5
+
6
+ [compare changes](https://github.com/ymansurozer/agents-vue/compare/v0.3.0...v0.4.0)
7
+
8
+ ### πŸš€ Enhancements
9
+
10
+ - `sendOnlyNewMessages` option: a send carries only the messages after the last one the server is known to hold, naming it as `afterMessageId` in the request body, rather than the whole transcript. An edit, or a bare `sendMessage()` resubmitting after an approval or a tool output, carries the message it rewrote; a regenerate carries the full list. The server must rebuild the full list before it persists or broadcasts the request: when the first message sent is one it stores, its stored messages before that one, then the ones sent; otherwise its whole stored transcript, then the sent messages it does not already store, so a client that missed another's messages does not erase them. The option is off by default, and with it off a request body is unchanged.
11
+
12
+ ## v0.3.0
13
+
14
+ Rebased onto AI SDK v7 and `agents` 0.27, with upstream's own transport and the four client-side bugs the Vue port carried fixed against the React client.
15
+
16
+ ### ⚠️ Breaking changes
17
+
18
+ - Peer dependencies: `ai ^7`, `@ai-sdk/vue ^4`, `agents >=0.27.0 <1`, `vue ^3.5`
19
+ - `WebSocketChatTransport` and `WebSocketLike` are no longer exported: the hand-rolled transport is deleted and the composable wires upstream's `WebSocketChatTransport` from `agents/chat/transport`. Import it from `agents` if you were using it directly.
20
+
21
+ ### 🩹 Fixes
22
+
23
+ - Client-side tool calls are dispatched off the rendered transcript, deduped by tool-call id, rather than off the AI SDK's own callback alone. A call in a turn this client only observed β€” a resumed turn, a turn another tab started β€” used to render as a chip and never run.
24
+ - An early `cf_agent_stream_resuming` is either fed to the continuation this client armed or observed as the server's own turn, never both. Doing both delivered the same turn twice.
25
+ - The continuation is armed before the `cf_agent_tool_result` / `cf_agent_tool_approval` frame is sent, so the common case does not race at all.
26
+ - A streaming assistant tail that matches nothing in an incoming sync is dropped rather than appended, which is what put the same turn on screen twice.
27
+ - Message ids are minted with `crypto.randomUUID` and otherwise from `crypto.getRandomValues`, never `Math.random`.
28
+ - A turn another tab started can be stopped from the watching tab, and an errored observed turn completes: the chunk handler registers a turn this client is only watching, and the completion branch reads an error frame as an ending rather than waiting for a done that never comes.
29
+ - A continuation handed to the server is released when the socket carrying that turn is replaced: the reconnect probe runs for exactly that case, and a `cf_agent_stream_resume_none` settles the continuation it answers. Without it a dropped socket left the page unable to auto-continue after any later tool result.
30
+
31
+ ### πŸš€ Enhancements
32
+
33
+ - `onFrame` option: every socket frame, parsed, before the chat protocol reads it; return `true` to consume it. This is where an application's own frames belong.
34
+ - `mergeMessages` option: how a reconnect's read of the stored transcript joins the one on screen. Defaults to merge-by-id with the stored copy winning.
35
+ - `onStateUpdate` forwarded to the `AgentClient`, `setAgent()` exposed, `connectionState` derived from the socket's own open/close, `retryPendingResume()` on open, and an async `get-messages` seed with a re-read on a replacement socket.
36
+
4
37
  ## v0.2.1
5
38
 
6
39
  [compare changes](https://github.com/ymansurozer/agents-vue/compare/v0.2.0...v0.2.1)
package/README.md CHANGED
@@ -6,7 +6,9 @@ Vue 3 composable for [Cloudflare Agents](https://github.com/cloudflare/agents)
6
6
 
7
7
  ## Status
8
8
 
9
- Covers the surface needed for a working chat UI β€” streaming responses, client-side tools, tool approvals, stream resumption, broadcast handling β€” at parity with upstream `@cloudflare/ai-chat` 0.9.x on resume/recovery: resume dedupe, `isRecovering`, `cancelOnClientAbort`, pre-stream `cf_agent_stream_pending`, and `connectionError`. Deeper upstream features (the `useAgent` connection hook, initial-message HTTP cache, `isServerStreaming` / `isStreaming` flags) are on the roadmap.
9
+ Covers the surface needed for a working chat UI β€” streaming responses, client-side tools, tool approvals, stream resumption, broadcast handling β€” at parity with upstream `@cloudflare/ai-chat` 0.12.x on resume/recovery: resume dedupe, `isRecovering`, `cancelOnClientAbort`, pre-stream `cf_agent_stream_pending`, and `connectionError`. Deeper upstream features (the `useAgent` connection hook, the cross-tab observer, `isServerStreaming` / `isStreaming` flags) are on the roadmap.
10
+
11
+ 0.3.0 moves the transport to upstream's own `WebSocketChatTransport` from `agents/chat/transport` and fixes four client-side bugs β€” see [CHANGELOG](./CHANGELOG.md).
10
12
 
11
13
  ## Install
12
14
 
@@ -18,10 +20,10 @@ Peer dependencies (must also be installed):
18
20
 
19
21
  | Peer | Version |
20
22
  |---|---|
21
- | `vue` | `^3.3.4` |
22
- | `@ai-sdk/vue` | `^3.0.0` |
23
- | `ai` | `^6.0.0` |
24
- | `agents` | `>=0.17.0 <1.0.0` |
23
+ | `vue` | `^3.5.0` |
24
+ | `@ai-sdk/vue` | `^4.0.0` |
25
+ | `ai` | `^7.0.0` |
26
+ | `agents` | `>=0.27.0 <1.0.0` |
25
27
 
26
28
  ## Usage
27
29
 
@@ -98,6 +100,98 @@ Returns:
98
100
  | `onClose` | `(event: CloseEvent) => void` | β€” | WebSocket close. |
99
101
  | `onSocketError` | `(event: Event) => void` | β€” | Connection-level error. |
100
102
  | `onError` | `(error: Error) => void` | β€” | Chat-turn error from the AI SDK. |
103
+ | `onStateUpdate` | `(state: State, source: "server" \| "client") => void` | β€” | The agent's state, on connect and on every change the server broadcasts. |
104
+ | `onFrame` | `(frame: unknown) => boolean \| void` | β€” | Every socket frame, parsed, **before** the chat protocol reads it. Return `true` to consume it. |
105
+ | `mergeMessages` | `(onScreen, stored) => ChatMessage[]` | merge by id | How a reconnect's read of the stored transcript joins the one on screen. |
106
+ | `sendOnlyNewMessages` | `boolean` | `false` | A send carries only the messages after the last one the server is known to hold, named as `afterMessageId`. The server must rebuild the rest. |
107
+
108
+ ### `onFrame` β€” your own frames
109
+
110
+ The chat protocol parser narrows a frame to the fields `cf_agent_*` messages have and drops every
111
+ other one, so a frame of your own would arrive with its contents removed. `onFrame` runs first and
112
+ sees the frame whole; returning `true` consumes it, and the protocol handling never sees it.
113
+
114
+ ```ts
115
+ useAgentChat({
116
+ client: { /* ... */ },
117
+ onFrame: (frame) => {
118
+ if (!isRecord(frame) || frame.type !== "my_app_event") return;
119
+ showBanner(frame);
120
+ return true; // consumed
121
+ },
122
+ });
123
+ ```
124
+
125
+ ### `mergeMessages` β€” what a reconnect does to the transcript
126
+
127
+ Nothing pushes a snapshot on connect, so a replacement socket re-reads the stored transcript β€” and
128
+ that read is a round trip of its own: it can be issued before a turn is persisted and land after the
129
+ socket has already delivered the finished answer. The default merges by id with the stored copy
130
+ winning and anything only on screen kept after it. Pass your own policy to decide differently.
131
+
132
+ ```ts
133
+ import type { UIMessage } from "ai";
134
+
135
+ const textLength = (message: UIMessage) =>
136
+ message.parts.reduce((total, part) => total + (part.type === "text" ? part.text.length : 0), 0);
137
+
138
+ /** The stored copy can be behind the stream, so the longer of the two wins. */
139
+ const fresher = (stored: UIMessage, onScreen: UIMessage | undefined) =>
140
+ onScreen && textLength(onScreen) > textLength(stored) ? onScreen : stored;
141
+
142
+ useAgentChat({
143
+ client: { /* ... */ },
144
+ mergeMessages: (onScreen, stored) => {
145
+ const live = new Map(onScreen.map((message) => [message.id, message]));
146
+ const storedIds = new Set(stored.map((message) => message.id));
147
+
148
+ return [
149
+ ...stored.map((message) => fresher(message, live.get(message.id))),
150
+ // Anything only on screen β€” a question typed while the socket was down β€” is kept after.
151
+ ...onScreen.filter((message) => !storedIds.has(message.id)),
152
+ ];
153
+ },
154
+ });
155
+ ```
156
+
157
+ ### `sendOnlyNewMessages` β€” sending what the server does not have
158
+
159
+ By default every send carries the whole transcript, so a request grows with the session. With
160
+ `sendOnlyNewMessages: true` it carries only the messages after the last one the server is known to
161
+ hold β€” usually just the new question β€” and names that message as `afterMessageId`, beside
162
+ `messages` in the request body. Known to hold means seen arriving from the server: `initialMessages`,
163
+ the `get-messages` read, a broadcast, a `cf_agent_message_updated` frame, or a stream that finished
164
+ without an error with a part in its reply. A message only this client has never counts. A request
165
+ that rewrites a message in place carries it too: an edited question, or the assistant message a
166
+ bare `sendMessage()` resubmits after an approval or a tool output. A regenerate, and a send before
167
+ the server is known to hold anything, carry the full list and no `afterMessageId`.
168
+
169
+ A client tool's output is not part of any request: it reaches the server in a `cf_agent_tool_result`
170
+ frame, with the option on or off. What changes is the next request. A question typed after a tool
171
+ output carries only the question, where the full list re-sent the output along with everything
172
+ else; only a request a bare `sendMessage()` makes carries the rewritten assistant message.
173
+
174
+ **The server's half.** A request with `afterMessageId` holds only part of the transcript, so the
175
+ server must rebuild the full list before it persists the request or broadcasts it β€” `AIChatAgent`
176
+ broadcasts a request's list to the other clients as it arrived. When the first message sent is one
177
+ it already stores, at index `k`, the request rewrites in place, and the full list is its stored
178
+ messages before `k`, then the ones sent. Otherwise the messages sent are new and go after the whole
179
+ stored transcript, less any it already stores, so a client that missed messages another client
180
+ added does not erase them. The rebuild reads `afterMessageId` only to know the list is partial: its
181
+ value, the last message the client saw the server hold, is a hint, and can name a reply the server
182
+ never stored. A request without the field carries the full list, as before. `AIChatAgent` does none
183
+ of this on its own: it takes a request's list for the whole transcript and deletes the stored
184
+ messages missing from it, which is why the option is off by default.
185
+
186
+ ```ts
187
+ function rebuild(stored: UIMessage[], sent: UIMessage[], afterMessageId?: string): UIMessage[] {
188
+ if (afterMessageId === undefined) return sent;
189
+ const rewriteAt = stored.findIndex((message) => message.id === sent[0]?.id);
190
+ if (rewriteAt >= 0) return [...stored.slice(0, rewriteAt), ...sent];
191
+ const storedIds = new Set(stored.map((message) => message.id));
192
+ return [...stored, ...sent.filter((message) => !storedIds.has(message.id))];
193
+ }
194
+ ```
101
195
 
102
196
  ### Client-side tools
103
197
 
@@ -133,25 +227,27 @@ The composable matches the approval id against a `tool-approve` part in `chat.me
133
227
 
134
228
  ### `WebSocketChatTransport`
135
229
 
136
- Exported for advanced use cases. The composable wires this internally; you only need it directly if you're building a non-`@ai-sdk/vue` flow over the same `cf_agent_*` wire protocol.
230
+ No longer exported from here. Since 0.3.0 the composable wires upstream's own transport, so import
231
+ it from `agents` directly if you are building a non-`@ai-sdk/vue` flow over the same `cf_agent_*`
232
+ wire protocol:
137
233
 
138
234
  ```ts
139
- import { WebSocketChatTransport, type WebSocketLike } from "agents-vue";
235
+ import { WebSocketChatTransport } from "agents/chat/transport";
140
236
 
141
237
  const transport = new WebSocketChatTransport({
142
- socket, // any WebSocketLike
238
+ agent, // an AgentClient
143
239
  prepareBody, // optional
144
240
  });
145
241
  ```
146
242
 
147
243
  ## Coverage vs upstream
148
244
 
149
- `agents-vue@0.1` covers the surface real apps use. Wire protocol is identical to upstream β€” same `cf_agent_*` message shapes, same resumable streaming, same broadcast transitions, same tool continuation flow.
245
+ `agents-vue` covers the surface real apps use. Wire protocol is identical to upstream β€” same `cf_agent_*` message shapes, same resumable streaming, same broadcast transitions, same tool continuation flow.
150
246
 
151
247
  | Feature | Status |
152
248
  |---|---|
153
249
  | `useAgentChat` composable | βœ… |
154
- | `WebSocketChatTransport` | βœ… |
250
+ | `WebSocketChatTransport` (upstream's, from `agents/chat/transport`) | βœ… |
155
251
  | Streaming responses | βœ… |
156
252
  | Client-side tools with `execute` | βœ… |
157
253
  | Tool approvals (HITL) | βœ… |
@@ -166,8 +262,16 @@ const transport = new WebSocketChatTransport({
166
262
  | Pre-stream `cf_agent_stream_pending` (extended resume probe) | βœ… |
167
263
  | `connectionError` (terminal WebSocket close) | βœ… |
168
264
  | Reconnect resume serialization (in-flight gate, `agents` 0.17.2 / #1837) | βœ… |
265
+ | `setAgent()`, which invalidates the transport's resume generation | βœ… |
266
+ | `connectionState`, from the socket's own open/close | βœ… |
267
+ | `onStateUpdate`, forwarded to the `AgentClient` | βœ… |
268
+ | Initial `get-messages` seed, and a re-read on a replacement socket | βœ… |
269
+ | Client-side tool calls dispatched off the rendered transcript | βœ… |
270
+ | Early-continuation branch (observe the announced turn, or launch β€” never both) | βœ… |
271
+ | Continuation armed before the tool frame is sent | βœ… |
272
+ | An unmatchable streaming tail dropped rather than appended | βœ… |
169
273
  | `useAgent` connection hook | 🚧 roadmap |
170
- | Initial-message HTTP cache | 🚧 roadmap |
274
+ | Cross-tab observer | 🚧 roadmap |
171
275
  | `isServerStreaming` / `isStreaming` flags | 🚧 roadmap |
172
276
 
173
277
  ## Contributing
@@ -175,7 +279,7 @@ const transport = new WebSocketChatTransport({
175
279
  ```bash
176
280
  pnpm install
177
281
  pnpm exec playwright install chromium
178
- pnpm test # 80 tests in chromium (vitest-browser-vue)
282
+ pnpm test # chromium (vitest-browser-vue)
179
283
  pnpm typecheck
180
284
  pnpm lint
181
285
  pnpm build
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as vue from 'vue';
2
2
  import * as ai from 'ai';
3
- import { UIMessage, Tool, JSONSchema7, ChatTransport, ChatRequestOptions, UIMessageChunk } from 'ai';
3
+ import { UIMessage, Tool, JSONSchema7 } from 'ai';
4
4
  import { Chat } from '@ai-sdk/vue';
5
5
  import { AgentClientOptions, AgentClient, AgentConnectionError } from 'agents/client';
6
6
 
@@ -53,6 +53,54 @@ interface UseAgentChatOptions<AgentT = unknown, ChatMessage extends UIMessage =
53
53
  onSocketError?: (event: Event) => void;
54
54
  /** Called for chat-turn errors (AI SDK). */
55
55
  onError?: (error: Error) => void;
56
+ /**
57
+ * Called with every socket frame, parsed, **before** the chat protocol reads it. Return `true`
58
+ * to consume the frame, which stops the protocol handling below from seeing it at all.
59
+ *
60
+ * This is where an application's own frames belong. The protocol parser narrows a frame to the
61
+ * fields the chat protocol has and drops every other one, so a frame of your own would otherwise
62
+ * arrive with its contents removed.
63
+ */
64
+ onFrame?: (frame: unknown) => boolean | void;
65
+ /**
66
+ * How a transcript read back from the server is merged into the one on screen, used when a
67
+ * replacement socket re-reads `get-messages`.
68
+ *
69
+ * The read is a round trip of its own: it can be issued before a turn is persisted and land
70
+ * after the socket has already delivered the finished answer, so replacing the transcript with
71
+ * it wholesale would erase a reply that is on screen. The default merges by id with the stored
72
+ * copy winning and anything only on screen appended after; pass your own policy to decide
73
+ * differently β€” which copy of a message is fresher, say.
74
+ */
75
+ mergeMessages?: (onScreen: ChatMessage[], stored: ChatMessage[]) => ChatMessage[];
76
+ /**
77
+ * When true, a send carries only the messages after the last one the server is known to hold, and
78
+ * names that message in the request body as `afterMessageId`. Known to hold means seen arriving
79
+ * from the server β€” in `initialMessages`, the `get-messages` read, a `cf_agent_chat_messages`
80
+ * broadcast, a `cf_agent_message_updated` frame, or a stream that finished without an error with
81
+ * a part in its reply β€” never a message only this client has.
82
+ *
83
+ * A request that rewrites a message in place carries it too: an edited question, or the assistant
84
+ * message a bare `sendMessage()` resubmits after an approval or a tool output. A tool output itself
85
+ * travels in its own frame, so a question typed after one no longer carries it. A regenerate, and a
86
+ * send before the server is known to hold anything, carry the full list and no `afterMessageId`.
87
+ *
88
+ * Turn it on only against a server that rebuilds the transcript before it persists or broadcasts
89
+ * the request: when the first message sent is one it stores, its stored messages before that one,
90
+ * then the ones sent; otherwise its whole stored transcript, then the sent messages it does not
91
+ * already store, so a client that missed another's messages does not erase them. `AIChatAgent` on
92
+ * its own takes a request's list for the whole transcript, and would keep only the messages sent.
93
+ */
94
+ sendOnlyNewMessages?: boolean;
95
+ /**
96
+ * Called with the agent's state, on connect and on every change the server broadcasts.
97
+ *
98
+ * The same option the React hook passes to `useAgent`. It is forwarded to `AgentClient`, which
99
+ * owns the `cf_agent_state` frame; the dispatcher below never sees one. `source` is `"server"` for
100
+ * a broadcast and `"client"` for this client's own `setState` β€” which a server-owned state refuses,
101
+ * so nothing here sends one.
102
+ */
103
+ onStateUpdate?: (state: State, source: "server" | "client") => void;
56
104
  }
57
105
  declare function useAgentChat<AgentT = unknown, ChatMessage extends UIMessage = UIMessage, State = StateOf<AgentT>>(options: UseAgentChatOptions<AgentT, ChatMessage, State>): {
58
106
  addToolApprovalResponse: (args_0: {
@@ -77,104 +125,17 @@ declare function useAgentChat<AgentT = unknown, ChatMessage extends UIMessage =
77
125
  clearHistory: () => void;
78
126
  chat: Chat<ChatMessage>;
79
127
  socket: AgentClient<AgentT, State>;
128
+ setAgent: (next: AgentClient<AgentT, State>) => void;
80
129
  stop: () => Promise<void>;
130
+ /** Reactive: true once the `get-messages` seed has settled (or was skipped). */
131
+ initialMessagesLoaded: vue.Ref<boolean, boolean>;
81
132
  /** Reactive: true while the server is recovering a durable chat turn. */
82
133
  isRecovering: vue.Ref<boolean, boolean>;
83
134
  /** Reactive: terminal WebSocket connection failure, or null. */
84
135
  connectionError: vue.Ref<AgentConnectionError | null, AgentConnectionError | null>;
136
+ /** Reactive: whether the socket is up, for the header's "Reconnecting…". */
137
+ connectionState: vue.Ref<"connected" | "reconnecting", "connected" | "reconnecting">;
85
138
  };
86
139
 
87
- /** Minimal interface a WebSocket must satisfy for the transport. */
88
- interface WebSocketLike {
89
- send(data: string): void;
90
- addEventListener(type: string, listener: (event: MessageEvent) => void, options?: AddEventListenerOptions): void;
91
- }
92
- interface StreamResumingData {
93
- type: string;
94
- id: string;
95
- }
96
- interface WebSocketChatTransportOptions {
97
- socket: WebSocketLike;
98
- activeRequestIds?: Set<string>;
99
- prepareBody?: (options: {
100
- messages: UIMessage[];
101
- trigger: "submit-message" | "regenerate-message";
102
- messageId?: string;
103
- }) => Record<string, unknown> | Promise<Record<string, unknown>>;
104
- /**
105
- * Whether a generic client-side abort (e.g. the AI SDK aborting the request
106
- * signal, or the stream being cancelled on teardown) should cancel the
107
- * *server* turn. Local-only by default β€” the server turn keeps running and can
108
- * be resumed/observed. Explicit `stop()` always cancels via
109
- * `cancelActiveServerTurn()`, regardless of this flag. Matches upstream 0.7.0.
110
- * @default false
111
- */
112
- cancelOnClientAbort?: boolean;
113
- }
114
- /**
115
- * ChatTransport that communicates with a Cloudflare Agent over WebSocket.
116
- * Mirrors the protocol implemented by `@cloudflare/ai-chat`'s
117
- * `WebSocketChatTransport`: same `cf_agent_*` message shapes, same resumable
118
- * streaming handshake, same tool continuation flow. Framework-agnostic β€” the
119
- * Vue composable consumes this; another framework binding could too.
120
- */
121
- declare class WebSocketChatTransport implements ChatTransport<UIMessage> {
122
- private socket;
123
- private prepareBody?;
124
- private activeRequestIds;
125
- _resumeResolver: ((data: StreamResumingData) => void) | null;
126
- _resumeNoneResolver: (() => void) | null;
127
- private _expectToolContinuation;
128
- private _abortToolContinuation;
129
- private cancelOnClientAbort;
130
- private _activeServerTurnId;
131
- private _cancelAttachedStream;
132
- private _onStreamPending;
133
- constructor(options: WebSocketChatTransportOptions);
134
- /** Mark that the next reconnectToStream() should attach to a server-initiated tool continuation. */
135
- expectToolContinuation(): void;
136
- /** Abort the active client-side tool continuation stream. */
137
- abortActiveToolContinuation(): boolean;
138
- /** Send a server-turn cancel frame for a request id (best-effort). */
139
- private sendCancelFrame;
140
- private setActiveServerTurn;
141
- private clearActiveServerTurn;
142
- /** Register a server-observed (broadcast / resume-fallback) turn as the cancellable one. */
143
- observeServerTurn(requestId: string): void;
144
- /** Mark a server turn complete so it is no longer the cancellable turn. */
145
- handleServerTurnCompleted(requestId: string): void;
146
- /**
147
- * Explicitly cancel the active server turn (and any tool continuation). Unlike a
148
- * generic client abort, this always signals the server. Drives `useAgentChat.stop()`.
149
- */
150
- cancelActiveServerTurn(): boolean;
151
- /** Called by the composable on cf_agent_stream_pending β€” extend the in-flight resume probe. */
152
- handleStreamPending(): boolean;
153
- /** True when the transport is waiting for a resume handshake. */
154
- isAwaitingResume(): boolean;
155
- /** Called by the composable when it receives cf_agent_stream_resuming. */
156
- handleStreamResuming(data: StreamResumingData): boolean;
157
- /** Called by the composable when it receives cf_agent_stream_resume_none. */
158
- handleStreamResumeNone(): boolean;
159
- sendMessages(options: {
160
- trigger: "submit-message" | "regenerate-message";
161
- chatId: string;
162
- messageId: string | undefined;
163
- messages: UIMessage[];
164
- abortSignal: AbortSignal | undefined;
165
- } & ChatRequestOptions): Promise<ReadableStream<UIMessageChunk>>;
166
- reconnectToStream(_options: {
167
- chatId: string;
168
- } & ChatRequestOptions): Promise<ReadableStream<UIMessageChunk> | null>;
169
- /**
170
- * Creates a deferred ReadableStream for client-side tool continuations.
171
- * Returned immediately so AI SDK status becomes "submitted" right after tool output,
172
- * then waits for the server to announce continuation via STREAM_RESUMING.
173
- */
174
- private _createToolContinuationStream;
175
- /** Creates a ReadableStream that receives resumed stream chunks. */
176
- private _createResumeStream;
177
- }
178
-
179
- export { WebSocketChatTransport, useAgentChat };
180
- export type { UseAgentChatOptions, WebSocketChatTransportOptions, WebSocketLike };
140
+ export { useAgentChat };
141
+ export type { UseAgentChatOptions };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as vue from 'vue';
2
2
  import * as ai from 'ai';
3
- import { UIMessage, Tool, JSONSchema7, ChatTransport, ChatRequestOptions, UIMessageChunk } from 'ai';
3
+ import { UIMessage, Tool, JSONSchema7 } from 'ai';
4
4
  import { Chat } from '@ai-sdk/vue';
5
5
  import { AgentClientOptions, AgentClient, AgentConnectionError } from 'agents/client';
6
6
 
@@ -53,6 +53,54 @@ interface UseAgentChatOptions<AgentT = unknown, ChatMessage extends UIMessage =
53
53
  onSocketError?: (event: Event) => void;
54
54
  /** Called for chat-turn errors (AI SDK). */
55
55
  onError?: (error: Error) => void;
56
+ /**
57
+ * Called with every socket frame, parsed, **before** the chat protocol reads it. Return `true`
58
+ * to consume the frame, which stops the protocol handling below from seeing it at all.
59
+ *
60
+ * This is where an application's own frames belong. The protocol parser narrows a frame to the
61
+ * fields the chat protocol has and drops every other one, so a frame of your own would otherwise
62
+ * arrive with its contents removed.
63
+ */
64
+ onFrame?: (frame: unknown) => boolean | void;
65
+ /**
66
+ * How a transcript read back from the server is merged into the one on screen, used when a
67
+ * replacement socket re-reads `get-messages`.
68
+ *
69
+ * The read is a round trip of its own: it can be issued before a turn is persisted and land
70
+ * after the socket has already delivered the finished answer, so replacing the transcript with
71
+ * it wholesale would erase a reply that is on screen. The default merges by id with the stored
72
+ * copy winning and anything only on screen appended after; pass your own policy to decide
73
+ * differently β€” which copy of a message is fresher, say.
74
+ */
75
+ mergeMessages?: (onScreen: ChatMessage[], stored: ChatMessage[]) => ChatMessage[];
76
+ /**
77
+ * When true, a send carries only the messages after the last one the server is known to hold, and
78
+ * names that message in the request body as `afterMessageId`. Known to hold means seen arriving
79
+ * from the server β€” in `initialMessages`, the `get-messages` read, a `cf_agent_chat_messages`
80
+ * broadcast, a `cf_agent_message_updated` frame, or a stream that finished without an error with
81
+ * a part in its reply β€” never a message only this client has.
82
+ *
83
+ * A request that rewrites a message in place carries it too: an edited question, or the assistant
84
+ * message a bare `sendMessage()` resubmits after an approval or a tool output. A tool output itself
85
+ * travels in its own frame, so a question typed after one no longer carries it. A regenerate, and a
86
+ * send before the server is known to hold anything, carry the full list and no `afterMessageId`.
87
+ *
88
+ * Turn it on only against a server that rebuilds the transcript before it persists or broadcasts
89
+ * the request: when the first message sent is one it stores, its stored messages before that one,
90
+ * then the ones sent; otherwise its whole stored transcript, then the sent messages it does not
91
+ * already store, so a client that missed another's messages does not erase them. `AIChatAgent` on
92
+ * its own takes a request's list for the whole transcript, and would keep only the messages sent.
93
+ */
94
+ sendOnlyNewMessages?: boolean;
95
+ /**
96
+ * Called with the agent's state, on connect and on every change the server broadcasts.
97
+ *
98
+ * The same option the React hook passes to `useAgent`. It is forwarded to `AgentClient`, which
99
+ * owns the `cf_agent_state` frame; the dispatcher below never sees one. `source` is `"server"` for
100
+ * a broadcast and `"client"` for this client's own `setState` β€” which a server-owned state refuses,
101
+ * so nothing here sends one.
102
+ */
103
+ onStateUpdate?: (state: State, source: "server" | "client") => void;
56
104
  }
57
105
  declare function useAgentChat<AgentT = unknown, ChatMessage extends UIMessage = UIMessage, State = StateOf<AgentT>>(options: UseAgentChatOptions<AgentT, ChatMessage, State>): {
58
106
  addToolApprovalResponse: (args_0: {
@@ -77,104 +125,17 @@ declare function useAgentChat<AgentT = unknown, ChatMessage extends UIMessage =
77
125
  clearHistory: () => void;
78
126
  chat: Chat<ChatMessage>;
79
127
  socket: AgentClient<AgentT, State>;
128
+ setAgent: (next: AgentClient<AgentT, State>) => void;
80
129
  stop: () => Promise<void>;
130
+ /** Reactive: true once the `get-messages` seed has settled (or was skipped). */
131
+ initialMessagesLoaded: vue.Ref<boolean, boolean>;
81
132
  /** Reactive: true while the server is recovering a durable chat turn. */
82
133
  isRecovering: vue.Ref<boolean, boolean>;
83
134
  /** Reactive: terminal WebSocket connection failure, or null. */
84
135
  connectionError: vue.Ref<AgentConnectionError | null, AgentConnectionError | null>;
136
+ /** Reactive: whether the socket is up, for the header's "Reconnecting…". */
137
+ connectionState: vue.Ref<"connected" | "reconnecting", "connected" | "reconnecting">;
85
138
  };
86
139
 
87
- /** Minimal interface a WebSocket must satisfy for the transport. */
88
- interface WebSocketLike {
89
- send(data: string): void;
90
- addEventListener(type: string, listener: (event: MessageEvent) => void, options?: AddEventListenerOptions): void;
91
- }
92
- interface StreamResumingData {
93
- type: string;
94
- id: string;
95
- }
96
- interface WebSocketChatTransportOptions {
97
- socket: WebSocketLike;
98
- activeRequestIds?: Set<string>;
99
- prepareBody?: (options: {
100
- messages: UIMessage[];
101
- trigger: "submit-message" | "regenerate-message";
102
- messageId?: string;
103
- }) => Record<string, unknown> | Promise<Record<string, unknown>>;
104
- /**
105
- * Whether a generic client-side abort (e.g. the AI SDK aborting the request
106
- * signal, or the stream being cancelled on teardown) should cancel the
107
- * *server* turn. Local-only by default β€” the server turn keeps running and can
108
- * be resumed/observed. Explicit `stop()` always cancels via
109
- * `cancelActiveServerTurn()`, regardless of this flag. Matches upstream 0.7.0.
110
- * @default false
111
- */
112
- cancelOnClientAbort?: boolean;
113
- }
114
- /**
115
- * ChatTransport that communicates with a Cloudflare Agent over WebSocket.
116
- * Mirrors the protocol implemented by `@cloudflare/ai-chat`'s
117
- * `WebSocketChatTransport`: same `cf_agent_*` message shapes, same resumable
118
- * streaming handshake, same tool continuation flow. Framework-agnostic β€” the
119
- * Vue composable consumes this; another framework binding could too.
120
- */
121
- declare class WebSocketChatTransport implements ChatTransport<UIMessage> {
122
- private socket;
123
- private prepareBody?;
124
- private activeRequestIds;
125
- _resumeResolver: ((data: StreamResumingData) => void) | null;
126
- _resumeNoneResolver: (() => void) | null;
127
- private _expectToolContinuation;
128
- private _abortToolContinuation;
129
- private cancelOnClientAbort;
130
- private _activeServerTurnId;
131
- private _cancelAttachedStream;
132
- private _onStreamPending;
133
- constructor(options: WebSocketChatTransportOptions);
134
- /** Mark that the next reconnectToStream() should attach to a server-initiated tool continuation. */
135
- expectToolContinuation(): void;
136
- /** Abort the active client-side tool continuation stream. */
137
- abortActiveToolContinuation(): boolean;
138
- /** Send a server-turn cancel frame for a request id (best-effort). */
139
- private sendCancelFrame;
140
- private setActiveServerTurn;
141
- private clearActiveServerTurn;
142
- /** Register a server-observed (broadcast / resume-fallback) turn as the cancellable one. */
143
- observeServerTurn(requestId: string): void;
144
- /** Mark a server turn complete so it is no longer the cancellable turn. */
145
- handleServerTurnCompleted(requestId: string): void;
146
- /**
147
- * Explicitly cancel the active server turn (and any tool continuation). Unlike a
148
- * generic client abort, this always signals the server. Drives `useAgentChat.stop()`.
149
- */
150
- cancelActiveServerTurn(): boolean;
151
- /** Called by the composable on cf_agent_stream_pending β€” extend the in-flight resume probe. */
152
- handleStreamPending(): boolean;
153
- /** True when the transport is waiting for a resume handshake. */
154
- isAwaitingResume(): boolean;
155
- /** Called by the composable when it receives cf_agent_stream_resuming. */
156
- handleStreamResuming(data: StreamResumingData): boolean;
157
- /** Called by the composable when it receives cf_agent_stream_resume_none. */
158
- handleStreamResumeNone(): boolean;
159
- sendMessages(options: {
160
- trigger: "submit-message" | "regenerate-message";
161
- chatId: string;
162
- messageId: string | undefined;
163
- messages: UIMessage[];
164
- abortSignal: AbortSignal | undefined;
165
- } & ChatRequestOptions): Promise<ReadableStream<UIMessageChunk>>;
166
- reconnectToStream(_options: {
167
- chatId: string;
168
- } & ChatRequestOptions): Promise<ReadableStream<UIMessageChunk> | null>;
169
- /**
170
- * Creates a deferred ReadableStream for client-side tool continuations.
171
- * Returned immediately so AI SDK status becomes "submitted" right after tool output,
172
- * then waits for the server to announce continuation via STREAM_RESUMING.
173
- */
174
- private _createToolContinuationStream;
175
- /** Creates a ReadableStream that receives resumed stream chunks. */
176
- private _createResumeStream;
177
- }
178
-
179
- export { WebSocketChatTransport, useAgentChat };
180
- export type { UseAgentChatOptions, WebSocketChatTransportOptions, WebSocketLike };
140
+ export { useAgentChat };
141
+ export type { UseAgentChatOptions };