agents-vue 0.1.0 β 0.2.1
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 +35 -0
- package/README.md +13 -4
- package/dist/index.d.mts +41 -1
- package/dist/index.d.ts +41 -1
- package/dist/index.mjs +211 -29
- package/package.json +15 -14
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
## v0.2.1
|
|
5
|
+
|
|
6
|
+
[compare changes](https://github.com/ymansurozer/agents-vue/compare/v0.2.0...v0.2.1)
|
|
7
|
+
|
|
8
|
+
### π©Ή Fixes
|
|
9
|
+
|
|
10
|
+
- Serialize reconnect resume re-probes (parity with agents 0.17.2, #1837) ([#1837](https://github.com/ymansurozer/agents-vue/issues/1837))
|
|
11
|
+
|
|
12
|
+
### β€οΈ Contributors
|
|
13
|
+
|
|
14
|
+
- Ymansurozer <ymansurozer@gmail.com>
|
|
15
|
+
|
|
16
|
+
## v0.2.0
|
|
17
|
+
|
|
18
|
+
[compare changes](https://github.com/ymansurozer/agents-vue/compare/v0.1.0...v0.2.0)
|
|
19
|
+
|
|
20
|
+
Brings the Vue port to parity with `@cloudflare/ai-chat` 0.9.x on resume/recovery.
|
|
21
|
+
|
|
22
|
+
### π Enhancements
|
|
23
|
+
|
|
24
|
+
- Resume dedupe: fallback-ACK a resume offer at most once per socket, preventing duplicated assistant text on a double resume replay (upstream 0.8.5)
|
|
25
|
+
- `isRecovering` reactive flag, driven by the `cf_agent_chat_recovering` frame and distinct from streaming (upstream 0.8.0)
|
|
26
|
+
- `cancelOnClientAbort` option: generic client aborts are local-only by default; explicit `stop()` always cancels the server turn β including resume-fallback-observed and transport-resumed turns (upstream 0.7.0)
|
|
27
|
+
- `cf_agent_stream_pending` handling: extend the resume probe 5s β 60s during the server's pre-stream window (upstream 0.9.0)
|
|
28
|
+
- `connectionError` surfaced from the `AgentClient` on terminal WebSocket close (upstream 0.9.0)
|
|
29
|
+
|
|
30
|
+
### π‘ Chore
|
|
31
|
+
|
|
32
|
+
- Bump `agents` to 0.17.0 and raise the peer floor to `>=0.17.0` (first version exporting `connectionError` / `onConnectionError`)
|
|
33
|
+
|
|
34
|
+
### β€οΈ Contributors
|
|
35
|
+
|
|
36
|
+
- Ymansurozer <ymansurozer@gmail.com>
|
|
37
|
+
|
|
38
|
+
|
|
4
39
|
## v0.1.0
|
|
5
40
|
|
|
6
41
|
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Vue 3 composable for [Cloudflare Agents](https://github.com/cloudflare/agents)
|
|
|
6
6
|
|
|
7
7
|
## Status
|
|
8
8
|
|
|
9
|
-
|
|
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.
|
|
10
10
|
|
|
11
11
|
## Install
|
|
12
12
|
|
|
@@ -21,7 +21,7 @@ Peer dependencies (must also be installed):
|
|
|
21
21
|
| `vue` | `^3.3.4` |
|
|
22
22
|
| `@ai-sdk/vue` | `^3.0.0` |
|
|
23
23
|
| `ai` | `^6.0.0` |
|
|
24
|
-
| `agents` | `>=0.
|
|
24
|
+
| `agents` | `>=0.17.0 <1.0.0` |
|
|
25
25
|
|
|
26
26
|
## Usage
|
|
27
27
|
|
|
@@ -78,7 +78,9 @@ Returns:
|
|
|
78
78
|
| `addToolOutput` | `function` | Submit a tool result. Sends `cf_agent_tool_result` and triggers continuation if enabled. |
|
|
79
79
|
| `addToolApprovalResponse` | `function` | Resolve a human-in-the-loop tool approval. Sends `cf_agent_tool_approval` and triggers continuation. |
|
|
80
80
|
| `clearHistory` | `function` | Clear local state and broadcast `cf_agent_chat_clear` to the server. |
|
|
81
|
-
| `stop` | `function` | Abort the active stream and any pending tool continuation. |
|
|
81
|
+
| `stop` | `function` | Abort the active stream and any pending tool continuation. Always cancels the server turn (explicit cancel). |
|
|
82
|
+
| `isRecovering` | `Ref<boolean>` | Reactive β `true` while the server is recovering a durable chat turn (distinct from streaming, so a UI can show "recoveringβ¦"). Driven by `cf_agent_chat_recovering`. |
|
|
83
|
+
| `connectionError` | `Ref<AgentConnectionError \| null>` | Reactive β the terminal WebSocket connection failure, or `null`. Cleared on reconnect. |
|
|
82
84
|
|
|
83
85
|
### Options
|
|
84
86
|
|
|
@@ -91,6 +93,7 @@ Returns:
|
|
|
91
93
|
| `onToolCall` | `({ toolCall, addToolOutput }) => void \| Promise<void>` | β | Custom tool-call handler. Overrides per-tool `execute`. |
|
|
92
94
|
| `autoContinueAfterToolResult` | `boolean` | `true` | Server resumes the stream after a tool result. |
|
|
93
95
|
| `resume` | `boolean` | `true` | Resume in-flight streams on socket reconnect. |
|
|
96
|
+
| `cancelOnClientAbort` | `boolean` | `false` | When `true`, a generic client-side abort cancels the server turn; when `false` (default) such aborts are local-only and the server turn keeps running and can resume. Explicit `stop()` always cancels. |
|
|
94
97
|
| `onOpen` | `(event: Event) => void` | β | WebSocket open. |
|
|
95
98
|
| `onClose` | `(event: CloseEvent) => void` | β | WebSocket close. |
|
|
96
99
|
| `onSocketError` | `(event: Event) => void` | β | Connection-level error. |
|
|
@@ -157,6 +160,12 @@ const transport = new WebSocketChatTransport({
|
|
|
157
160
|
| Partial-hydration replay rebuild | β
|
|
|
158
161
|
| Broadcast handling (multi-tab) | β
|
|
|
159
162
|
| Tool approval continuation merge across broadcasts | β
|
|
|
163
|
+
| Resume dedupe (fallback-ACK at most once per socket) | β
|
|
|
164
|
+
| `isRecovering` durable-recovery hint (`cf_agent_chat_recovering`) | β
|
|
|
165
|
+
| `cancelOnClientAbort` (local-only abort, default) | β
|
|
|
166
|
+
| Pre-stream `cf_agent_stream_pending` (extended resume probe) | β
|
|
|
167
|
+
| `connectionError` (terminal WebSocket close) | β
|
|
|
168
|
+
| Reconnect resume serialization (in-flight gate, `agents` 0.17.2 / #1837) | β
|
|
|
160
169
|
| `useAgent` connection hook | π§ roadmap |
|
|
161
170
|
| Initial-message HTTP cache | π§ roadmap |
|
|
162
171
|
| `isServerStreaming` / `isStreaming` flags | π§ roadmap |
|
|
@@ -166,7 +175,7 @@ const transport = new WebSocketChatTransport({
|
|
|
166
175
|
```bash
|
|
167
176
|
pnpm install
|
|
168
177
|
pnpm exec playwright install chromium
|
|
169
|
-
pnpm test #
|
|
178
|
+
pnpm test # 80 tests in chromium (vitest-browser-vue)
|
|
170
179
|
pnpm typecheck
|
|
171
180
|
pnpm lint
|
|
172
181
|
pnpm build
|
package/dist/index.d.mts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import * as vue from 'vue';
|
|
1
2
|
import * as ai from 'ai';
|
|
2
3
|
import { UIMessage, Tool, JSONSchema7, ChatTransport, ChatRequestOptions, UIMessageChunk } from 'ai';
|
|
3
4
|
import { Chat } from '@ai-sdk/vue';
|
|
4
|
-
import { AgentClientOptions, AgentClient } from 'agents/client';
|
|
5
|
+
import { AgentClientOptions, AgentClient, AgentConnectionError } from 'agents/client';
|
|
5
6
|
|
|
6
7
|
interface AgentClientTool<Input = unknown, Output = unknown> {
|
|
7
8
|
description?: Tool["description"];
|
|
@@ -37,6 +38,13 @@ interface UseAgentChatOptions<AgentT = unknown, ChatMessage extends UIMessage =
|
|
|
37
38
|
autoContinueAfterToolResult?: boolean;
|
|
38
39
|
/** When true (default), the chat resumes any in-flight stream on socket open. */
|
|
39
40
|
resume?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* When true, a generic client-side abort (e.g. the AI SDK aborting the request on
|
|
43
|
+
* teardown) cancels the server turn. When false (default), such aborts are
|
|
44
|
+
* local-only β the server turn keeps running and can be resumed. Explicit `stop()`
|
|
45
|
+
* always cancels the server turn regardless. Matches upstream 0.7.0.
|
|
46
|
+
*/
|
|
47
|
+
cancelOnClientAbort?: boolean;
|
|
40
48
|
/** Called when the underlying WebSocket opens (initial connect or reconnect). */
|
|
41
49
|
onOpen?: (event: Event) => void;
|
|
42
50
|
/** Called when the underlying WebSocket closes. */
|
|
@@ -70,6 +78,10 @@ declare function useAgentChat<AgentT = unknown, ChatMessage extends UIMessage =
|
|
|
70
78
|
chat: Chat<ChatMessage>;
|
|
71
79
|
socket: AgentClient<AgentT, State>;
|
|
72
80
|
stop: () => Promise<void>;
|
|
81
|
+
/** Reactive: true while the server is recovering a durable chat turn. */
|
|
82
|
+
isRecovering: vue.Ref<boolean, boolean>;
|
|
83
|
+
/** Reactive: terminal WebSocket connection failure, or null. */
|
|
84
|
+
connectionError: vue.Ref<AgentConnectionError | null, AgentConnectionError | null>;
|
|
73
85
|
};
|
|
74
86
|
|
|
75
87
|
/** Minimal interface a WebSocket must satisfy for the transport. */
|
|
@@ -89,6 +101,15 @@ interface WebSocketChatTransportOptions {
|
|
|
89
101
|
trigger: "submit-message" | "regenerate-message";
|
|
90
102
|
messageId?: string;
|
|
91
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;
|
|
92
113
|
}
|
|
93
114
|
/**
|
|
94
115
|
* ChatTransport that communicates with a Cloudflare Agent over WebSocket.
|
|
@@ -105,11 +126,30 @@ declare class WebSocketChatTransport implements ChatTransport<UIMessage> {
|
|
|
105
126
|
_resumeNoneResolver: (() => void) | null;
|
|
106
127
|
private _expectToolContinuation;
|
|
107
128
|
private _abortToolContinuation;
|
|
129
|
+
private cancelOnClientAbort;
|
|
130
|
+
private _activeServerTurnId;
|
|
131
|
+
private _cancelAttachedStream;
|
|
132
|
+
private _onStreamPending;
|
|
108
133
|
constructor(options: WebSocketChatTransportOptions);
|
|
109
134
|
/** Mark that the next reconnectToStream() should attach to a server-initiated tool continuation. */
|
|
110
135
|
expectToolContinuation(): void;
|
|
111
136
|
/** Abort the active client-side tool continuation stream. */
|
|
112
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;
|
|
113
153
|
/** True when the transport is waiting for a resume handshake. */
|
|
114
154
|
isAwaitingResume(): boolean;
|
|
115
155
|
/** Called by the composable when it receives cf_agent_stream_resuming. */
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import * as vue from 'vue';
|
|
1
2
|
import * as ai from 'ai';
|
|
2
3
|
import { UIMessage, Tool, JSONSchema7, ChatTransport, ChatRequestOptions, UIMessageChunk } from 'ai';
|
|
3
4
|
import { Chat } from '@ai-sdk/vue';
|
|
4
|
-
import { AgentClientOptions, AgentClient } from 'agents/client';
|
|
5
|
+
import { AgentClientOptions, AgentClient, AgentConnectionError } from 'agents/client';
|
|
5
6
|
|
|
6
7
|
interface AgentClientTool<Input = unknown, Output = unknown> {
|
|
7
8
|
description?: Tool["description"];
|
|
@@ -37,6 +38,13 @@ interface UseAgentChatOptions<AgentT = unknown, ChatMessage extends UIMessage =
|
|
|
37
38
|
autoContinueAfterToolResult?: boolean;
|
|
38
39
|
/** When true (default), the chat resumes any in-flight stream on socket open. */
|
|
39
40
|
resume?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* When true, a generic client-side abort (e.g. the AI SDK aborting the request on
|
|
43
|
+
* teardown) cancels the server turn. When false (default), such aborts are
|
|
44
|
+
* local-only β the server turn keeps running and can be resumed. Explicit `stop()`
|
|
45
|
+
* always cancels the server turn regardless. Matches upstream 0.7.0.
|
|
46
|
+
*/
|
|
47
|
+
cancelOnClientAbort?: boolean;
|
|
40
48
|
/** Called when the underlying WebSocket opens (initial connect or reconnect). */
|
|
41
49
|
onOpen?: (event: Event) => void;
|
|
42
50
|
/** Called when the underlying WebSocket closes. */
|
|
@@ -70,6 +78,10 @@ declare function useAgentChat<AgentT = unknown, ChatMessage extends UIMessage =
|
|
|
70
78
|
chat: Chat<ChatMessage>;
|
|
71
79
|
socket: AgentClient<AgentT, State>;
|
|
72
80
|
stop: () => Promise<void>;
|
|
81
|
+
/** Reactive: true while the server is recovering a durable chat turn. */
|
|
82
|
+
isRecovering: vue.Ref<boolean, boolean>;
|
|
83
|
+
/** Reactive: terminal WebSocket connection failure, or null. */
|
|
84
|
+
connectionError: vue.Ref<AgentConnectionError | null, AgentConnectionError | null>;
|
|
73
85
|
};
|
|
74
86
|
|
|
75
87
|
/** Minimal interface a WebSocket must satisfy for the transport. */
|
|
@@ -89,6 +101,15 @@ interface WebSocketChatTransportOptions {
|
|
|
89
101
|
trigger: "submit-message" | "regenerate-message";
|
|
90
102
|
messageId?: string;
|
|
91
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;
|
|
92
113
|
}
|
|
93
114
|
/**
|
|
94
115
|
* ChatTransport that communicates with a Cloudflare Agent over WebSocket.
|
|
@@ -105,11 +126,30 @@ declare class WebSocketChatTransport implements ChatTransport<UIMessage> {
|
|
|
105
126
|
_resumeNoneResolver: (() => void) | null;
|
|
106
127
|
private _expectToolContinuation;
|
|
107
128
|
private _abortToolContinuation;
|
|
129
|
+
private cancelOnClientAbort;
|
|
130
|
+
private _activeServerTurnId;
|
|
131
|
+
private _cancelAttachedStream;
|
|
132
|
+
private _onStreamPending;
|
|
108
133
|
constructor(options: WebSocketChatTransportOptions);
|
|
109
134
|
/** Mark that the next reconnectToStream() should attach to a server-initiated tool continuation. */
|
|
110
135
|
expectToolContinuation(): void;
|
|
111
136
|
/** Abort the active client-side tool continuation stream. */
|
|
112
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;
|
|
113
153
|
/** True when the transport is waiting for a resume handshake. */
|
|
114
154
|
isAwaitingResume(): boolean;
|
|
115
155
|
/** Called by the composable when it receives cf_agent_stream_resuming. */
|
package/dist/index.mjs
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { Chat } from '@ai-sdk/vue';
|
|
2
2
|
import { broadcastTransition } from 'agents/chat';
|
|
3
3
|
import { AgentClient } from 'agents/client';
|
|
4
|
-
import { onScopeDispose } from 'vue';
|
|
4
|
+
import { ref, onScopeDispose } from 'vue';
|
|
5
5
|
|
|
6
6
|
function randomRequestId() {
|
|
7
7
|
return Math.random().toString(36).slice(2, 10);
|
|
8
8
|
}
|
|
9
|
+
const RESUME_PROBE_TIMEOUT_MS = 5e3;
|
|
10
|
+
const RESUME_PENDING_TIMEOUT_MS = 6e4;
|
|
9
11
|
class WebSocketChatTransport {
|
|
10
12
|
socket;
|
|
11
13
|
prepareBody;
|
|
@@ -16,10 +18,20 @@ class WebSocketChatTransport {
|
|
|
16
18
|
// Tool continuation state
|
|
17
19
|
_expectToolContinuation = false;
|
|
18
20
|
_abortToolContinuation = null;
|
|
21
|
+
// Whether a generic client abort should cancel the server turn (default: local-only).
|
|
22
|
+
cancelOnClientAbort;
|
|
23
|
+
// The currently cancellable server turn β either locally initiated (sendMessages)
|
|
24
|
+
// or observed via the broadcast/resume-fallback path. `_cancelAttachedStream` tears
|
|
25
|
+
// down the local stream attached to a locally-initiated turn (null for observed turns).
|
|
26
|
+
_activeServerTurnId = null;
|
|
27
|
+
_cancelAttachedStream = null;
|
|
28
|
+
// Pre-stream "pending" extender for the in-flight resume probe (cleared on resolve).
|
|
29
|
+
_onStreamPending = null;
|
|
19
30
|
constructor(options) {
|
|
20
31
|
this.socket = options.socket;
|
|
21
32
|
this.prepareBody = options.prepareBody;
|
|
22
33
|
this.activeRequestIds = options.activeRequestIds ?? /* @__PURE__ */ new Set();
|
|
34
|
+
this.cancelOnClientAbort = options.cancelOnClientAbort ?? false;
|
|
23
35
|
}
|
|
24
36
|
/** Mark that the next reconnectToStream() should attach to a server-initiated tool continuation. */
|
|
25
37
|
expectToolContinuation() {
|
|
@@ -29,6 +41,55 @@ class WebSocketChatTransport {
|
|
|
29
41
|
abortActiveToolContinuation() {
|
|
30
42
|
return this._abortToolContinuation?.() ?? false;
|
|
31
43
|
}
|
|
44
|
+
/** Send a server-turn cancel frame for a request id (best-effort). */
|
|
45
|
+
sendCancelFrame(id) {
|
|
46
|
+
try {
|
|
47
|
+
this.socket.send(JSON.stringify({ id, type: "cf_agent_chat_request_cancel" }));
|
|
48
|
+
} catch {
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
setActiveServerTurn(id, cancelStream) {
|
|
52
|
+
this._activeServerTurnId = id;
|
|
53
|
+
this._cancelAttachedStream = cancelStream;
|
|
54
|
+
}
|
|
55
|
+
clearActiveServerTurn(id) {
|
|
56
|
+
if (this._activeServerTurnId === id) {
|
|
57
|
+
this._activeServerTurnId = null;
|
|
58
|
+
this._cancelAttachedStream = null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** Register a server-observed (broadcast / resume-fallback) turn as the cancellable one. */
|
|
62
|
+
observeServerTurn(requestId) {
|
|
63
|
+
this.setActiveServerTurn(requestId, null);
|
|
64
|
+
}
|
|
65
|
+
/** Mark a server turn complete so it is no longer the cancellable turn. */
|
|
66
|
+
handleServerTurnCompleted(requestId) {
|
|
67
|
+
this.clearActiveServerTurn(requestId);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Explicitly cancel the active server turn (and any tool continuation). Unlike a
|
|
71
|
+
* generic client abort, this always signals the server. Drives `useAgentChat.stop()`.
|
|
72
|
+
*/
|
|
73
|
+
cancelActiveServerTurn() {
|
|
74
|
+
const id = this._activeServerTurnId;
|
|
75
|
+
let cancelledRequest = false;
|
|
76
|
+
if (id) {
|
|
77
|
+
this.sendCancelFrame(id);
|
|
78
|
+
this._cancelAttachedStream?.();
|
|
79
|
+
this.clearActiveServerTurn(id);
|
|
80
|
+
cancelledRequest = true;
|
|
81
|
+
}
|
|
82
|
+
const cancelledToolContinuation = this.abortActiveToolContinuation();
|
|
83
|
+
return cancelledRequest || cancelledToolContinuation;
|
|
84
|
+
}
|
|
85
|
+
/** Called by the composable on cf_agent_stream_pending β extend the in-flight resume probe. */
|
|
86
|
+
handleStreamPending() {
|
|
87
|
+
if (!this._onStreamPending) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
this._onStreamPending();
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
32
93
|
/** True when the transport is waiting for a resume handshake. */
|
|
33
94
|
isAwaitingResume() {
|
|
34
95
|
return this._resumeResolver !== null || this._resumeNoneResolver !== null;
|
|
@@ -73,11 +134,14 @@ class WebSocketChatTransport {
|
|
|
73
134
|
this.activeRequestIds.add(requestId);
|
|
74
135
|
const socket = this.socket;
|
|
75
136
|
const activeIds = this.activeRequestIds;
|
|
76
|
-
const finish = (action, keepId = false) => {
|
|
137
|
+
const finish = (action, keepId = false, clearServerTurn = true) => {
|
|
77
138
|
if (completed) {
|
|
78
139
|
return;
|
|
79
140
|
}
|
|
80
141
|
completed = true;
|
|
142
|
+
if (clearServerTurn) {
|
|
143
|
+
this.clearActiveServerTurn(requestId);
|
|
144
|
+
}
|
|
81
145
|
try {
|
|
82
146
|
action();
|
|
83
147
|
} catch {
|
|
@@ -89,16 +153,27 @@ class WebSocketChatTransport {
|
|
|
89
153
|
};
|
|
90
154
|
const abortError = new Error("Aborted");
|
|
91
155
|
abortError.name = "AbortError";
|
|
156
|
+
let requestSent = false;
|
|
92
157
|
let streamController;
|
|
158
|
+
const cancelActiveRequest = () => {
|
|
159
|
+
if (completed) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
finish(() => streamController.error(abortError), true);
|
|
163
|
+
return true;
|
|
164
|
+
};
|
|
93
165
|
const onAbort = () => {
|
|
94
166
|
if (completed) {
|
|
95
167
|
return;
|
|
96
168
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
169
|
+
if (this.cancelOnClientAbort) {
|
|
170
|
+
if (requestSent) {
|
|
171
|
+
this.sendCancelFrame(requestId);
|
|
172
|
+
}
|
|
173
|
+
finish(() => streamController.error(abortError), requestSent);
|
|
174
|
+
} else {
|
|
175
|
+
finish(() => streamController.error(abortError), false, !requestSent);
|
|
100
176
|
}
|
|
101
|
-
finish(() => streamController.error(abortError), true);
|
|
102
177
|
};
|
|
103
178
|
const stream = new ReadableStream({
|
|
104
179
|
start(controller) {
|
|
@@ -135,19 +210,23 @@ class WebSocketChatTransport {
|
|
|
135
210
|
onAbort();
|
|
136
211
|
}
|
|
137
212
|
});
|
|
213
|
+
this.setActiveServerTurn(requestId, cancelActiveRequest);
|
|
138
214
|
if (options.abortSignal) {
|
|
139
215
|
options.abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
140
216
|
if (options.abortSignal.aborted) {
|
|
141
217
|
onAbort();
|
|
142
218
|
}
|
|
143
219
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
220
|
+
if (!completed) {
|
|
221
|
+
socket.send(
|
|
222
|
+
JSON.stringify({
|
|
223
|
+
id: requestId,
|
|
224
|
+
init: { method: "POST", body: bodyPayload },
|
|
225
|
+
type: "cf_agent_use_chat_request"
|
|
226
|
+
})
|
|
227
|
+
);
|
|
228
|
+
requestSent = true;
|
|
229
|
+
}
|
|
151
230
|
return stream;
|
|
152
231
|
}
|
|
153
232
|
async reconnectToStream(_options) {
|
|
@@ -158,6 +237,7 @@ class WebSocketChatTransport {
|
|
|
158
237
|
const activeIds = this.activeRequestIds;
|
|
159
238
|
return new Promise((resolve) => {
|
|
160
239
|
let resolved = false;
|
|
240
|
+
let timeout;
|
|
161
241
|
const done = (value) => {
|
|
162
242
|
if (resolved) {
|
|
163
243
|
return;
|
|
@@ -165,6 +245,7 @@ class WebSocketChatTransport {
|
|
|
165
245
|
resolved = true;
|
|
166
246
|
this._resumeResolver = null;
|
|
167
247
|
this._resumeNoneResolver = null;
|
|
248
|
+
this._onStreamPending = null;
|
|
168
249
|
clearTimeout(timeout);
|
|
169
250
|
resolve(value);
|
|
170
251
|
};
|
|
@@ -175,11 +256,18 @@ class WebSocketChatTransport {
|
|
|
175
256
|
this.socket.send(JSON.stringify({ type: "cf_agent_stream_resume_ack", id }));
|
|
176
257
|
done(this._createResumeStream(id));
|
|
177
258
|
};
|
|
259
|
+
this._onStreamPending = () => {
|
|
260
|
+
if (resolved) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
clearTimeout(timeout);
|
|
264
|
+
timeout = setTimeout(() => done(null), RESUME_PENDING_TIMEOUT_MS);
|
|
265
|
+
};
|
|
178
266
|
try {
|
|
179
267
|
this.socket.send(JSON.stringify({ type: "cf_agent_stream_resume_request" }));
|
|
180
268
|
} catch {
|
|
181
269
|
}
|
|
182
|
-
|
|
270
|
+
timeout = setTimeout(() => done(null), RESUME_PROBE_TIMEOUT_MS);
|
|
183
271
|
});
|
|
184
272
|
}
|
|
185
273
|
/**
|
|
@@ -217,6 +305,7 @@ class WebSocketChatTransport {
|
|
|
217
305
|
}
|
|
218
306
|
completed = true;
|
|
219
307
|
this._abortToolContinuation = null;
|
|
308
|
+
this._onStreamPending = null;
|
|
220
309
|
clearHandshakeResolvers(resumeResolver, resumeNoneResolver);
|
|
221
310
|
try {
|
|
222
311
|
action();
|
|
@@ -256,12 +345,26 @@ class WebSocketChatTransport {
|
|
|
256
345
|
requestId = data.id;
|
|
257
346
|
activeIds.add(requestId);
|
|
258
347
|
clearHandshakeResolvers(onResume, onResumeNone);
|
|
348
|
+
this._onStreamPending = null;
|
|
259
349
|
clearTimeout(timeout);
|
|
260
350
|
socket.send(JSON.stringify({ type: "cf_agent_stream_resume_ack", id: requestId }));
|
|
261
351
|
};
|
|
262
352
|
onResumeRef = onResume;
|
|
263
353
|
onResumeNoneRef = onResumeNone;
|
|
264
|
-
|
|
354
|
+
let timeout = setTimeout(
|
|
355
|
+
() => finish(() => controller.close(), onResume, onResumeNone),
|
|
356
|
+
RESUME_PROBE_TIMEOUT_MS
|
|
357
|
+
);
|
|
358
|
+
this._onStreamPending = () => {
|
|
359
|
+
if (completed) {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
clearTimeout(timeout);
|
|
363
|
+
timeout = setTimeout(
|
|
364
|
+
() => finish(() => controller.close(), onResume, onResumeNone),
|
|
365
|
+
RESUME_PENDING_TIMEOUT_MS
|
|
366
|
+
);
|
|
367
|
+
};
|
|
265
368
|
this._resumeResolver = onResume;
|
|
266
369
|
this._resumeNoneResolver = onResumeNone;
|
|
267
370
|
const onMessage = (event) => {
|
|
@@ -294,9 +397,15 @@ class WebSocketChatTransport {
|
|
|
294
397
|
finish(() => controller.close());
|
|
295
398
|
}
|
|
296
399
|
},
|
|
297
|
-
cancel() {
|
|
298
|
-
|
|
299
|
-
|
|
400
|
+
cancel: () => {
|
|
401
|
+
if (requestId && this.cancelOnClientAbort) {
|
|
402
|
+
this.sendCancelFrame(requestId);
|
|
403
|
+
finish(() => {
|
|
404
|
+
}, onResumeRef, onResumeNoneRef, true);
|
|
405
|
+
} else {
|
|
406
|
+
finish(() => {
|
|
407
|
+
}, onResumeRef, onResumeNoneRef);
|
|
408
|
+
}
|
|
300
409
|
}
|
|
301
410
|
});
|
|
302
411
|
}
|
|
@@ -305,21 +414,30 @@ class WebSocketChatTransport {
|
|
|
305
414
|
const socket = this.socket;
|
|
306
415
|
const activeIds = this.activeRequestIds;
|
|
307
416
|
const chunkAbort = new AbortController();
|
|
417
|
+
const abortError = new Error("Aborted");
|
|
418
|
+
abortError.name = "AbortError";
|
|
308
419
|
let completed = false;
|
|
309
|
-
|
|
420
|
+
let streamController;
|
|
421
|
+
const finish = (action, keepId = false, clearServerTurn = true) => {
|
|
310
422
|
if (completed) {
|
|
311
423
|
return;
|
|
312
424
|
}
|
|
313
425
|
completed = true;
|
|
426
|
+
if (clearServerTurn) {
|
|
427
|
+
this.clearActiveServerTurn(requestId);
|
|
428
|
+
}
|
|
314
429
|
try {
|
|
315
430
|
action();
|
|
316
431
|
} catch {
|
|
317
432
|
}
|
|
318
|
-
|
|
433
|
+
if (!keepId) {
|
|
434
|
+
activeIds.delete(requestId);
|
|
435
|
+
}
|
|
319
436
|
chunkAbort.abort();
|
|
320
437
|
};
|
|
321
|
-
|
|
438
|
+
const stream = new ReadableStream({
|
|
322
439
|
start(controller) {
|
|
440
|
+
streamController = controller;
|
|
323
441
|
const onMessage = (event) => {
|
|
324
442
|
try {
|
|
325
443
|
const data = JSON.parse(String(event.data));
|
|
@@ -348,11 +466,25 @@ class WebSocketChatTransport {
|
|
|
348
466
|
};
|
|
349
467
|
socket.addEventListener("message", onMessage, { signal: chunkAbort.signal });
|
|
350
468
|
},
|
|
351
|
-
cancel() {
|
|
352
|
-
|
|
353
|
-
|
|
469
|
+
cancel: () => {
|
|
470
|
+
if (this.cancelOnClientAbort) {
|
|
471
|
+
this.sendCancelFrame(requestId);
|
|
472
|
+
finish(() => {
|
|
473
|
+
}, true);
|
|
474
|
+
} else {
|
|
475
|
+
finish(() => {
|
|
476
|
+
}, false, false);
|
|
477
|
+
}
|
|
354
478
|
}
|
|
355
479
|
});
|
|
480
|
+
this.setActiveServerTurn(requestId, () => {
|
|
481
|
+
if (completed) {
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
finish(() => streamController.error(abortError), true);
|
|
485
|
+
return true;
|
|
486
|
+
});
|
|
487
|
+
return stream;
|
|
356
488
|
}
|
|
357
489
|
}
|
|
358
490
|
|
|
@@ -375,6 +507,7 @@ function parseProtocolMessage(data) {
|
|
|
375
507
|
replay: typeof parsed.replay === "boolean" ? parsed.replay : void 0,
|
|
376
508
|
replayComplete: typeof parsed.replayComplete === "boolean" ? parsed.replayComplete : void 0,
|
|
377
509
|
continuation: typeof parsed.continuation === "boolean" ? parsed.continuation : void 0,
|
|
510
|
+
recovering: typeof parsed.recovering === "boolean" ? parsed.recovering : void 0,
|
|
378
511
|
message,
|
|
379
512
|
messages
|
|
380
513
|
};
|
|
@@ -479,12 +612,24 @@ function createMessageId() {
|
|
|
479
612
|
function useAgentChat(options) {
|
|
480
613
|
const autoContinueAfterToolResult = options.autoContinueAfterToolResult ?? true;
|
|
481
614
|
const activeRequestIds = /* @__PURE__ */ new Set();
|
|
615
|
+
const fallbackAckedResumeRequestIds = /* @__PURE__ */ new Set();
|
|
482
616
|
let streamState = { status: "idle" };
|
|
483
617
|
let isResumingToolContinuation = false;
|
|
484
|
-
|
|
618
|
+
let resumeInFlight = false;
|
|
619
|
+
let resumeGeneration = 0;
|
|
620
|
+
const isRecovering = ref(false);
|
|
621
|
+
const connectionError = ref(null);
|
|
622
|
+
const socket = new AgentClient({
|
|
623
|
+
...options.client,
|
|
624
|
+
onConnectionError: (error) => {
|
|
625
|
+
connectionError.value = error;
|
|
626
|
+
options.client.onConnectionError?.(error);
|
|
627
|
+
}
|
|
628
|
+
});
|
|
485
629
|
const transport = new WebSocketChatTransport({
|
|
486
630
|
socket,
|
|
487
631
|
activeRequestIds,
|
|
632
|
+
cancelOnClientAbort: options.cancelOnClientAbort ?? false,
|
|
488
633
|
prepareBody: async () => {
|
|
489
634
|
const rawBody = options.body;
|
|
490
635
|
const extraBody = typeof rawBody === "function" ? await rawBody() : rawBody ?? {};
|
|
@@ -531,6 +676,7 @@ function useAgentChat(options) {
|
|
|
531
676
|
}
|
|
532
677
|
async function stop() {
|
|
533
678
|
try {
|
|
679
|
+
transport.cancelActiveServerTurn();
|
|
534
680
|
await chat.stop();
|
|
535
681
|
} finally {
|
|
536
682
|
transport.abortActiveToolContinuation();
|
|
@@ -539,6 +685,7 @@ function useAgentChat(options) {
|
|
|
539
685
|
function clearHistory() {
|
|
540
686
|
void stop();
|
|
541
687
|
isResumingToolContinuation = false;
|
|
688
|
+
isRecovering.value = false;
|
|
542
689
|
streamState = broadcastTransition(streamState, { type: "clear" }).state;
|
|
543
690
|
chat.messages = [];
|
|
544
691
|
socket.send(JSON.stringify({ type: "cf_agent_chat_clear" }));
|
|
@@ -619,21 +766,33 @@ function useAgentChat(options) {
|
|
|
619
766
|
case "cf_agent_chat_clear":
|
|
620
767
|
streamState = broadcastTransition(streamState, { type: "clear" }).state;
|
|
621
768
|
chat.messages = [];
|
|
769
|
+
isRecovering.value = false;
|
|
622
770
|
break;
|
|
623
771
|
// Forward resume handshake to transport
|
|
624
772
|
case "cf_agent_stream_resuming":
|
|
625
|
-
if (data.id && !transport.handleStreamResuming({ type: data.type, id: data.id }) && !activeRequestIds.has(data.id)) {
|
|
773
|
+
if (data.id && !transport.handleStreamResuming({ type: data.type, id: data.id }) && !activeRequestIds.has(data.id) && !fallbackAckedResumeRequestIds.has(data.id)) {
|
|
626
774
|
streamState = broadcastTransition(streamState, {
|
|
627
775
|
type: "resume-fallback",
|
|
628
776
|
streamId: data.id,
|
|
629
777
|
messageId: createMessageId()
|
|
630
778
|
}).state;
|
|
779
|
+
fallbackAckedResumeRequestIds.add(data.id);
|
|
780
|
+
transport.observeServerTurn(data.id);
|
|
781
|
+
isRecovering.value = false;
|
|
631
782
|
socket.send(JSON.stringify({ type: "cf_agent_stream_resume_ack", id: data.id }));
|
|
632
783
|
}
|
|
633
784
|
break;
|
|
634
785
|
case "cf_agent_stream_resume_none":
|
|
635
786
|
transport.handleStreamResumeNone();
|
|
636
787
|
break;
|
|
788
|
+
// Pre-stream window (0.9.0): keep the in-flight resume probe waiting.
|
|
789
|
+
case "cf_agent_stream_pending":
|
|
790
|
+
transport.handleStreamPending();
|
|
791
|
+
break;
|
|
792
|
+
// Durable chat-recovery progress hint (0.8.0). Cleared on terminal/stream below.
|
|
793
|
+
case "cf_agent_chat_recovering":
|
|
794
|
+
isRecovering.value = Boolean(data.recovering);
|
|
795
|
+
break;
|
|
637
796
|
case "cf_agent_use_chat_response":
|
|
638
797
|
if (!data.id || activeRequestIds.has(data.id)) {
|
|
639
798
|
return;
|
|
@@ -656,6 +815,10 @@ function useAgentChat(options) {
|
|
|
656
815
|
if (result.messagesUpdate) {
|
|
657
816
|
chat.messages = result.messagesUpdate(chat.messages);
|
|
658
817
|
}
|
|
818
|
+
if (data.done) {
|
|
819
|
+
isRecovering.value = false;
|
|
820
|
+
transport.handleServerTurnCompleted(data.id);
|
|
821
|
+
}
|
|
659
822
|
}
|
|
660
823
|
break;
|
|
661
824
|
}
|
|
@@ -663,12 +826,23 @@ function useAgentChat(options) {
|
|
|
663
826
|
}
|
|
664
827
|
});
|
|
665
828
|
socket.addEventListener("open", (event) => {
|
|
829
|
+
connectionError.value = null;
|
|
666
830
|
options.onOpen?.(event);
|
|
667
|
-
if (options.resume !== false) {
|
|
668
|
-
|
|
831
|
+
if (options.resume !== false && !resumeInFlight && !isResumingToolContinuation && !transport.isAwaitingResume()) {
|
|
832
|
+
resumeInFlight = true;
|
|
833
|
+
const myGeneration = resumeGeneration;
|
|
834
|
+
void chat.resumeStream().catch(() => {
|
|
835
|
+
}).finally(() => {
|
|
836
|
+
if (resumeGeneration !== myGeneration) {
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
resumeInFlight = false;
|
|
840
|
+
});
|
|
669
841
|
}
|
|
670
842
|
});
|
|
671
843
|
socket.addEventListener("close", (event) => {
|
|
844
|
+
fallbackAckedResumeRequestIds.clear();
|
|
845
|
+
isRecovering.value = false;
|
|
672
846
|
if (options.onClose && event instanceof CloseEvent) {
|
|
673
847
|
options.onClose(event);
|
|
674
848
|
}
|
|
@@ -677,6 +851,10 @@ function useAgentChat(options) {
|
|
|
677
851
|
options.onSocketError?.(event);
|
|
678
852
|
});
|
|
679
853
|
onScopeDispose(() => {
|
|
854
|
+
fallbackAckedResumeRequestIds.clear();
|
|
855
|
+
isRecovering.value = false;
|
|
856
|
+
resumeGeneration++;
|
|
857
|
+
resumeInFlight = false;
|
|
680
858
|
socket.close();
|
|
681
859
|
});
|
|
682
860
|
return {
|
|
@@ -685,7 +863,11 @@ function useAgentChat(options) {
|
|
|
685
863
|
clearHistory,
|
|
686
864
|
chat,
|
|
687
865
|
socket,
|
|
688
|
-
stop
|
|
866
|
+
stop,
|
|
867
|
+
/** Reactive: true while the server is recovering a durable chat turn. */
|
|
868
|
+
isRecovering,
|
|
869
|
+
/** Reactive: terminal WebSocket connection failure, or null. */
|
|
870
|
+
connectionError
|
|
689
871
|
};
|
|
690
872
|
}
|
|
691
873
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agents-vue",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Vue 3 composable for Cloudflare Agents β a port of @cloudflare/ai-chat/react's useAgentChat.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"vue",
|
|
@@ -38,9 +38,19 @@
|
|
|
38
38
|
"README.md",
|
|
39
39
|
"CHANGELOG.md"
|
|
40
40
|
],
|
|
41
|
+
"scripts": {
|
|
42
|
+
"dev": "unbuild --watch",
|
|
43
|
+
"build": "unbuild",
|
|
44
|
+
"test": "vitest run",
|
|
45
|
+
"test:watch": "vitest",
|
|
46
|
+
"lint": "eslint .",
|
|
47
|
+
"lint:fix": "eslint . --fix",
|
|
48
|
+
"typecheck": "vue-tsc --noEmit",
|
|
49
|
+
"release": "pnpm run lint && pnpm run typecheck && pnpm run test && pnpm run build && changelogen --release && git push --follow-tags"
|
|
50
|
+
},
|
|
41
51
|
"peerDependencies": {
|
|
42
52
|
"@ai-sdk/vue": "^3.0.0",
|
|
43
|
-
"agents": ">=0.
|
|
53
|
+
"agents": ">=0.17.0 <1.0.0",
|
|
44
54
|
"ai": "^6.0.0",
|
|
45
55
|
"vue": "^3.3.4"
|
|
46
56
|
},
|
|
@@ -50,7 +60,7 @@
|
|
|
50
60
|
"@types/node": "^22.0.0",
|
|
51
61
|
"@vitest/browser": "^4.1.5",
|
|
52
62
|
"@vitest/browser-playwright": "^4.1.5",
|
|
53
|
-
"agents": "^0.
|
|
63
|
+
"agents": "^0.17.0",
|
|
54
64
|
"ai": "^6.0.0",
|
|
55
65
|
"changelogen": "^0.6.2",
|
|
56
66
|
"eslint": "^9.0.0",
|
|
@@ -69,14 +79,5 @@
|
|
|
69
79
|
"engines": {
|
|
70
80
|
"node": ">=20"
|
|
71
81
|
},
|
|
72
|
-
"
|
|
73
|
-
|
|
74
|
-
"build": "unbuild",
|
|
75
|
-
"test": "vitest run",
|
|
76
|
-
"test:watch": "vitest",
|
|
77
|
-
"lint": "eslint .",
|
|
78
|
-
"lint:fix": "eslint . --fix",
|
|
79
|
-
"typecheck": "vue-tsc --noEmit",
|
|
80
|
-
"release": "pnpm run lint && pnpm run typecheck && pnpm run test && pnpm run build && changelogen --release && git push --follow-tags"
|
|
81
|
-
}
|
|
82
|
-
}
|
|
82
|
+
"packageManager": "pnpm@9.0.0"
|
|
83
|
+
}
|