agents-vue 0.3.0 → 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 +8 -0
- package/README.md +40 -0
- package/dist/index.d.mts +19 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.mjs +65 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
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
|
+
|
|
4
12
|
## v0.3.0
|
|
5
13
|
|
|
6
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.
|
package/README.md
CHANGED
|
@@ -103,6 +103,7 @@ Returns:
|
|
|
103
103
|
| `onStateUpdate` | `(state: State, source: "server" \| "client") => void` | — | The agent's state, on connect and on every change the server broadcasts. |
|
|
104
104
|
| `onFrame` | `(frame: unknown) => boolean \| void` | — | Every socket frame, parsed, **before** the chat protocol reads it. Return `true` to consume it. |
|
|
105
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. |
|
|
106
107
|
|
|
107
108
|
### `onFrame` — your own frames
|
|
108
109
|
|
|
@@ -153,6 +154,45 @@ useAgentChat({
|
|
|
153
154
|
});
|
|
154
155
|
```
|
|
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
|
+
```
|
|
195
|
+
|
|
156
196
|
### Client-side tools
|
|
157
197
|
|
|
158
198
|
```ts
|
package/dist/index.d.mts
CHANGED
|
@@ -73,6 +73,25 @@ interface UseAgentChatOptions<AgentT = unknown, ChatMessage extends UIMessage =
|
|
|
73
73
|
* differently — which copy of a message is fresher, say.
|
|
74
74
|
*/
|
|
75
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;
|
|
76
95
|
/**
|
|
77
96
|
* Called with the agent's state, on connect and on every change the server broadcasts.
|
|
78
97
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -73,6 +73,25 @@ interface UseAgentChatOptions<AgentT = unknown, ChatMessage extends UIMessage =
|
|
|
73
73
|
* differently — which copy of a message is fresher, say.
|
|
74
74
|
*/
|
|
75
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;
|
|
76
95
|
/**
|
|
77
96
|
* Called with the agent's state, on connect and on every change the server broadcasts.
|
|
78
97
|
*
|
package/dist/index.mjs
CHANGED
|
@@ -128,6 +128,17 @@ function mergeById(onScreen, stored) {
|
|
|
128
128
|
const storedIds = new Set(stored.map((message) => message.id));
|
|
129
129
|
return [...stored, ...onScreen.filter((message) => !storedIds.has(message.id))];
|
|
130
130
|
}
|
|
131
|
+
function idsOf(messages) {
|
|
132
|
+
return messages.map((message) => message.id);
|
|
133
|
+
}
|
|
134
|
+
function startMessageIdOf(body) {
|
|
135
|
+
try {
|
|
136
|
+
const chunk = JSON.parse(body);
|
|
137
|
+
return isRecord(chunk) && chunk.type === "start" && typeof chunk.messageId === "string" ? chunk.messageId : null;
|
|
138
|
+
} catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
131
142
|
function createMessageId() {
|
|
132
143
|
if (globalThis.crypto?.randomUUID) {
|
|
133
144
|
return globalThis.crypto.randomUUID();
|
|
@@ -146,6 +157,8 @@ function useAgentChat(options) {
|
|
|
146
157
|
let continuationLaunchTimer = null;
|
|
147
158
|
let observedToolContinuationRequestId = null;
|
|
148
159
|
const dispatchedToolCallIds = /* @__PURE__ */ new Set();
|
|
160
|
+
let heldMessageIds = idsOf(options.initialMessages ?? []);
|
|
161
|
+
const streamMessageIds = /* @__PURE__ */ new Map();
|
|
149
162
|
let resumeInFlight = false;
|
|
150
163
|
let resumeGeneration = 0;
|
|
151
164
|
const isRecovering = ref(false);
|
|
@@ -198,9 +211,50 @@ function useAgentChat(options) {
|
|
|
198
211
|
}
|
|
199
212
|
const chat = new Chat({
|
|
200
213
|
messages: options.initialMessages,
|
|
201
|
-
transport
|
|
214
|
+
transport: options.sendOnlyNewMessages ? {
|
|
215
|
+
sendMessages: (request) => transport.sendMessages(onlyNewMessages(request)),
|
|
216
|
+
reconnectToStream: (request) => transport.reconnectToStream(request)
|
|
217
|
+
} : transport,
|
|
202
218
|
onError: options.onError
|
|
203
219
|
});
|
|
220
|
+
function onlyNewMessages(request) {
|
|
221
|
+
if (request.trigger === "regenerate-message") {
|
|
222
|
+
return request;
|
|
223
|
+
}
|
|
224
|
+
const { messages } = request;
|
|
225
|
+
let held = 0;
|
|
226
|
+
while (held < messages.length && messages[held]?.id === heldMessageIds[held]) {
|
|
227
|
+
held++;
|
|
228
|
+
}
|
|
229
|
+
const rewrittenIndex = messages.findIndex((message) => message.id === request.messageId);
|
|
230
|
+
const start = rewrittenIndex >= 0 ? Math.min(held, rewrittenIndex) : held;
|
|
231
|
+
if (rewrittenIndex >= 0) {
|
|
232
|
+
heldMessageIds = heldMessageIds.slice(0, start);
|
|
233
|
+
}
|
|
234
|
+
const afterMessageId = messages[start - 1]?.id;
|
|
235
|
+
if (afterMessageId === void 0 || start === messages.length) {
|
|
236
|
+
return request;
|
|
237
|
+
}
|
|
238
|
+
return { ...request, messages: messages.slice(start), body: { ...request.body, afterMessageId } };
|
|
239
|
+
}
|
|
240
|
+
function holdThrough(messageId) {
|
|
241
|
+
const index = chat.messages.findIndex((message) => message.id === messageId);
|
|
242
|
+
if (index >= 0) {
|
|
243
|
+
heldMessageIds = idsOf(chat.messages.slice(0, index + 1));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function trackStreamMessage(requestId, frame) {
|
|
247
|
+
if (frame.done || frame.error) {
|
|
248
|
+
const messageId = streamMessageIds.get(requestId);
|
|
249
|
+
streamMessageIds.delete(requestId);
|
|
250
|
+
const reply = chat.messages.find((message) => message.id === messageId);
|
|
251
|
+
if (reply && reply.parts.length > 0 && !frame.error) {
|
|
252
|
+
holdThrough(reply.id);
|
|
253
|
+
}
|
|
254
|
+
} else if (!streamMessageIds.has(requestId) && frame.body?.trim()) {
|
|
255
|
+
streamMessageIds.set(requestId, startMessageIdOf(frame.body));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
204
258
|
function dispatchRenderedToolCalls() {
|
|
205
259
|
if (chat.status === "streaming" || chat.status === "submitted" || isServerStreaming.value || isRecovering.value) {
|
|
206
260
|
return;
|
|
@@ -273,6 +327,7 @@ function useAgentChat(options) {
|
|
|
273
327
|
isServerStreaming.value = false;
|
|
274
328
|
streamState = broadcastTransition(streamState, { type: "clear" }).state;
|
|
275
329
|
chat.messages = [];
|
|
330
|
+
heldMessageIds = [];
|
|
276
331
|
socket.send(JSON.stringify({ type: "cf_agent_chat_clear" }));
|
|
277
332
|
}
|
|
278
333
|
async function addToolOutputForTool(toolName, toolOutput) {
|
|
@@ -348,17 +403,20 @@ function useAgentChat(options) {
|
|
|
348
403
|
// Full message list sync (sent on connect, and after every appendMessage / turn).
|
|
349
404
|
case "cf_agent_chat_messages":
|
|
350
405
|
chat.messages = chat.status === "streaming" || chat.status === "submitted" || streamState.status !== "idle" ? preserveActiveStreamMessages(chat.messages, data.messages ?? []) : data.messages ?? [];
|
|
406
|
+
heldMessageIds = idsOf(data.messages ?? []);
|
|
351
407
|
break;
|
|
352
408
|
// Single message update (e.g. tool result applied)
|
|
353
409
|
case "cf_agent_message_updated":
|
|
354
410
|
if (data.message) {
|
|
355
411
|
chat.messages = replaceUpdatedMessage(chat.messages, data.message);
|
|
412
|
+
holdThrough(data.message.id);
|
|
356
413
|
}
|
|
357
414
|
break;
|
|
358
415
|
// Clear all messages
|
|
359
416
|
case "cf_agent_chat_clear":
|
|
360
417
|
streamState = broadcastTransition(streamState, { type: "clear" }).state;
|
|
361
418
|
chat.messages = [];
|
|
419
|
+
heldMessageIds = [];
|
|
362
420
|
isRecovering.value = false;
|
|
363
421
|
isServerStreaming.value = false;
|
|
364
422
|
break;
|
|
@@ -405,6 +463,9 @@ function useAgentChat(options) {
|
|
|
405
463
|
isRecovering.value = Boolean(data.recovering);
|
|
406
464
|
break;
|
|
407
465
|
case "cf_agent_use_chat_response":
|
|
466
|
+
if (data.id) {
|
|
467
|
+
trackStreamMessage(data.id, data);
|
|
468
|
+
}
|
|
408
469
|
if (!data.id || activeRequestIds.has(data.id)) {
|
|
409
470
|
return;
|
|
410
471
|
}
|
|
@@ -470,6 +531,7 @@ function useAgentChat(options) {
|
|
|
470
531
|
});
|
|
471
532
|
socket.addEventListener("close", (event) => {
|
|
472
533
|
fallbackAckedResumeRequestIds.clear();
|
|
534
|
+
streamMessageIds.clear();
|
|
473
535
|
isRecovering.value = false;
|
|
474
536
|
connectionState.value = "reconnecting";
|
|
475
537
|
resyncPending = true;
|
|
@@ -521,6 +583,7 @@ function useAgentChat(options) {
|
|
|
521
583
|
const stored = await fetchStoredMessages();
|
|
522
584
|
if (stored && chat.messages.length === 0) {
|
|
523
585
|
chat.messages = stored;
|
|
586
|
+
heldMessageIds = idsOf(stored);
|
|
524
587
|
}
|
|
525
588
|
} finally {
|
|
526
589
|
initialMessagesLoaded.value = true;
|
|
@@ -530,6 +593,7 @@ function useAgentChat(options) {
|
|
|
530
593
|
const stored = await fetchStoredMessages();
|
|
531
594
|
if (stored) {
|
|
532
595
|
chat.messages = (options.mergeMessages ?? mergeById)(chat.messages, stored);
|
|
596
|
+
heldMessageIds = idsOf(stored);
|
|
533
597
|
}
|
|
534
598
|
}
|
|
535
599
|
return {
|