agents 0.16.1 → 0.17.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/README.md +11 -8
- package/dist/{agent-tool-types-NofdbL9X.d.ts → agent-tool-types-Cd1TZPfB.d.ts} +595 -137
- package/dist/agent-tool-types.d.ts +34 -18
- package/dist/agent-tool-types.js +20 -1
- package/dist/agent-tool-types.js.map +1 -0
- package/dist/agent-tools-BXlsuX0d.js +304 -0
- package/dist/agent-tools-BXlsuX0d.js.map +1 -0
- package/dist/agent-tools-_E8wxUIK.d.ts +119 -0
- package/dist/agent-tools.d.ts +24 -18
- package/dist/agent-tools.js.map +1 -1
- package/dist/ai-chat-agent.d.ts +1 -1
- package/dist/ai-chat-agent.js +1 -2
- package/dist/ai-chat-agent.js.map +1 -1
- package/dist/ai-chat-v5-migration.d.ts +1 -1
- package/dist/ai-chat-v5-migration.js +1 -2
- package/dist/ai-chat-v5-migration.js.map +1 -1
- package/dist/ai-react.d.ts +1 -1
- package/dist/ai-react.js +1 -2
- package/dist/ai-react.js.map +1 -1
- package/dist/ai-types.d.ts +1 -7
- package/dist/ai-types.js +2 -8
- package/dist/ai-types.js.map +1 -1
- package/dist/browser/ai.js +1 -1
- package/dist/browser/index.js +1 -1
- package/dist/chat/index.d.ts +1923 -76
- package/dist/chat/index.js +1784 -278
- package/dist/chat/index.js.map +1 -1
- package/dist/chat/react.d.ts +602 -0
- package/dist/chat/react.js +1506 -0
- package/dist/chat/react.js.map +1 -0
- package/dist/chat-sdk/index.d.ts +4 -4
- package/dist/{classPrivateFieldGet2-CZ7QjTXN.js → classPrivateFieldGet2-DZBYAB34.js} +5 -5
- package/dist/{classPrivateMethodInitSpec-D-0__zd9.js → classPrivateMethodInitSpec-qMjJ6sHQ.js} +2 -2
- package/dist/{client-FUizKzj2.js → client-BZ-B3NhC.js} +19 -3
- package/dist/client-BZ-B3NhC.js.map +1 -0
- package/dist/client-tools-aIBO0Fk7.d.ts +53 -0
- package/dist/client.d.ts +76 -57
- package/dist/client.js +33 -5
- package/dist/client.js.map +1 -1
- package/dist/{compaction-helpers-DVcu5lPN.d.ts → compaction-helpers-wUz6M3us.d.ts} +20 -1
- package/dist/{connector-CrKhowfD.js → connector-CdldGF3h.js} +5 -5
- package/dist/{connector-CrKhowfD.js.map → connector-CdldGF3h.js.map} +1 -1
- package/dist/email.js +1 -1
- package/dist/email.js.map +1 -1
- package/dist/experimental/memory/session/index.d.ts +1 -1
- package/dist/experimental/memory/session/index.js +20 -2
- package/dist/experimental/memory/session/index.js.map +1 -1
- package/dist/experimental/memory/utils/index.d.ts +1 -1
- package/dist/{index-B7IbEeze.d.ts → index-CcbnKkNh.d.ts} +188 -14
- package/dist/index.d.ts +91 -71
- package/dist/index.js +562 -24
- package/dist/index.js.map +1 -1
- package/dist/mcp/client.d.ts +18 -14
- package/dist/mcp/client.js +1 -1
- package/dist/mcp/index.d.ts +30 -30
- package/dist/mcp/index.js +7 -7
- package/dist/mcp/index.js.map +1 -1
- package/dist/{agent-tools-3zLG7MgA.js → message-builder-BymO4N_D.js} +45 -126
- package/dist/message-builder-BymO4N_D.js.map +1 -0
- package/dist/observability/index.d.ts +1 -1
- package/dist/observability/index.js +4 -2
- package/dist/observability/index.js.map +1 -1
- package/dist/react.d.ts +123 -110
- package/dist/react.js +44 -11
- package/dist/react.js.map +1 -1
- package/dist/serializable.d.ts +1 -1
- package/dist/skills/compile.js +1 -1
- package/dist/skills/compile.js.map +1 -1
- package/dist/skills/index.js +5 -5
- package/dist/skills/index.js.map +1 -1
- package/dist/sub-routing.d.ts +6 -6
- package/dist/utils.js +1 -1
- package/dist/utils.js.map +1 -1
- package/dist/vite.js +5 -5
- package/dist/vite.js.map +1 -1
- package/dist/wire-types-nflOzNuU.js +240 -0
- package/dist/wire-types-nflOzNuU.js.map +1 -0
- package/dist/{workflow-types-SrZK_o9p.d.ts → workflow-types-Baz_PO5v.d.ts} +42 -22
- package/dist/workflow-types.d.ts +25 -21
- package/dist/workflow-types.js.map +1 -1
- package/dist/workflows.d.ts +22 -21
- package/dist/workflows.js +31 -7
- package/dist/workflows.js.map +1 -1
- package/docs/adding-to-existing-project.md +450 -0
- package/docs/agent-class.md +503 -0
- package/docs/agent-tools.md +552 -0
- package/docs/browse-the-web.md +430 -0
- package/docs/callable-methods.md +627 -0
- package/docs/chat-agents.md +1687 -0
- package/docs/chat-sdk.md +181 -0
- package/docs/client-sdk.md +520 -0
- package/docs/client-tools-continuation.md +177 -0
- package/docs/codemode.md +440 -0
- package/docs/configuration.md +775 -0
- package/docs/cross-domain-authentication.md +171 -0
- package/docs/durable-execution.md +537 -0
- package/docs/email.md +663 -0
- package/docs/get-current-agent.md +204 -0
- package/docs/getting-started.md +305 -0
- package/docs/http-websockets.md +668 -0
- package/docs/human-in-the-loop.md +661 -0
- package/docs/index.md +151 -0
- package/docs/long-running-agents.md +730 -0
- package/docs/mcp-client.md +620 -0
- package/docs/mcp-servers.md +526 -0
- package/docs/mcp-transports.md +308 -0
- package/docs/migration-to-ai-sdk-v5.md +96 -0
- package/docs/migration-to-ai-sdk-v6.md +163 -0
- package/docs/observability.md +261 -0
- package/docs/push-notifications.md +367 -0
- package/docs/queue.md +329 -0
- package/docs/readonly-connections.md +278 -0
- package/docs/resumable-streaming.md +127 -0
- package/docs/retries.md +444 -0
- package/docs/routing.md +749 -0
- package/docs/scheduling.md +898 -0
- package/docs/securing-mcp-servers.md +359 -0
- package/docs/server-driven-messages.md +477 -0
- package/docs/sessions.md +1024 -0
- package/docs/state.md +512 -0
- package/docs/sub-agents.md +389 -0
- package/docs/webhooks.md +604 -0
- package/docs/workflows.md +877 -0
- package/package.json +47 -15
- package/dist/agent-tools-3zLG7MgA.js.map +0 -1
- package/dist/agent-tools-DLquv-dp.d.ts +0 -14
- package/dist/client-FUizKzj2.js.map +0 -1
|
@@ -0,0 +1,1506 @@
|
|
|
1
|
+
import { n as transition } from "../wire-types-nflOzNuU.js";
|
|
2
|
+
import { getToolName, isToolUIPart } from "ai";
|
|
3
|
+
import { nanoid } from "nanoid";
|
|
4
|
+
import { use, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
5
|
+
import { useChat } from "@ai-sdk/react";
|
|
6
|
+
//#region src/chat/ws-chat-transport.ts
|
|
7
|
+
/**
|
|
8
|
+
* Short safety-net timeout for a resume probe when the server has said nothing.
|
|
9
|
+
* Under normal operation the server answers a `STREAM_RESUME_REQUEST` with
|
|
10
|
+
* `STREAM_RESUMING`, `STREAM_RESUME_NONE`, or `STREAM_PENDING` well before this.
|
|
11
|
+
*/
|
|
12
|
+
const RESUME_PROBE_TIMEOUT_MS = 5e3;
|
|
13
|
+
/**
|
|
14
|
+
* Extended backstop applied once the server says a turn is pending
|
|
15
|
+
* (`STREAM_PENDING`, #1784). The pre-stream window (queueing, MCP setup,
|
|
16
|
+
* debounce, model latency) can exceed the short probe timeout, and the server
|
|
17
|
+
* guarantees a follow-up `STREAM_RESUMING` or `STREAM_RESUME_NONE` — so we wait
|
|
18
|
+
* much longer (refreshed on every keep-waiting frame) but still cap it so a
|
|
19
|
+
* dropped follow-up degrades to a null resolve instead of hanging forever.
|
|
20
|
+
*/
|
|
21
|
+
const RESUME_PENDING_TIMEOUT_MS = 6e4;
|
|
22
|
+
/**
|
|
23
|
+
* ChatTransport that sends messages over WebSocket and returns a
|
|
24
|
+
* ReadableStream<UIMessageChunk> that the AI SDK's useChat consumes directly.
|
|
25
|
+
* No fake fetch, no Response reconstruction, no double SSE parsing.
|
|
26
|
+
*/
|
|
27
|
+
var WebSocketChatTransport = class {
|
|
28
|
+
constructor(options) {
|
|
29
|
+
this._resumeResolver = null;
|
|
30
|
+
this._resumeNoneResolver = null;
|
|
31
|
+
this._onStreamPending = null;
|
|
32
|
+
this._expectToolContinuation = false;
|
|
33
|
+
this._abortToolContinuation = null;
|
|
34
|
+
this._activeServerTurnId = null;
|
|
35
|
+
this._cancelAttachedStream = null;
|
|
36
|
+
this.agent = options.agent;
|
|
37
|
+
this.prepareBody = options.prepareBody;
|
|
38
|
+
this.activeRequestIds = options.activeRequestIds;
|
|
39
|
+
this.cancelOnClientAbort = options.cancelOnClientAbort ?? false;
|
|
40
|
+
}
|
|
41
|
+
setCancelOnClientAbort(cancelOnClientAbort) {
|
|
42
|
+
this.cancelOnClientAbort = cancelOnClientAbort;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Explicitly cancel the active server turn, if any.
|
|
46
|
+
* This is separate from generic client-side abort/cancel lifecycle so
|
|
47
|
+
* clients can detach locally without stopping server work.
|
|
48
|
+
*/
|
|
49
|
+
cancelActiveServerTurn() {
|
|
50
|
+
const requestId = this._activeServerTurnId;
|
|
51
|
+
let cancelledRequest = false;
|
|
52
|
+
if (requestId) {
|
|
53
|
+
this.sendCancelFrame(requestId);
|
|
54
|
+
this._cancelAttachedStream?.();
|
|
55
|
+
this.clearActiveServerTurn(requestId);
|
|
56
|
+
cancelledRequest = true;
|
|
57
|
+
}
|
|
58
|
+
const cancelledToolContinuation = this.abortActiveToolContinuation();
|
|
59
|
+
return cancelledRequest || cancelledToolContinuation;
|
|
60
|
+
}
|
|
61
|
+
sendCancelFrame(requestId) {
|
|
62
|
+
try {
|
|
63
|
+
this.agent.send(JSON.stringify({
|
|
64
|
+
id: requestId,
|
|
65
|
+
type: "cf_agent_chat_request_cancel"
|
|
66
|
+
}));
|
|
67
|
+
} catch {}
|
|
68
|
+
}
|
|
69
|
+
setActiveServerTurn(requestId, cancelAttachedStream) {
|
|
70
|
+
this._activeServerTurnId = requestId;
|
|
71
|
+
this._cancelAttachedStream = cancelAttachedStream;
|
|
72
|
+
}
|
|
73
|
+
clearActiveServerTurn(requestId) {
|
|
74
|
+
if (this._activeServerTurnId === requestId) {
|
|
75
|
+
this._activeServerTurnId = null;
|
|
76
|
+
this._cancelAttachedStream = null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Mark that the next reconnectToStream() call should attach to a
|
|
81
|
+
* server-initiated tool continuation rather than a page-load resume.
|
|
82
|
+
*/
|
|
83
|
+
expectToolContinuation() {
|
|
84
|
+
this._expectToolContinuation = true;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Abort the active client-side tool continuation stream, if one is attached
|
|
88
|
+
* to a server request id.
|
|
89
|
+
*/
|
|
90
|
+
abortActiveToolContinuation() {
|
|
91
|
+
return this._abortToolContinuation?.() ?? false;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* True when the transport is waiting for a resume handshake.
|
|
95
|
+
*/
|
|
96
|
+
isAwaitingResume() {
|
|
97
|
+
return this._resumeResolver !== null || this._resumeNoneResolver !== null;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Called by onAgentMessage when it receives CF_AGENT_STREAM_RESUMING.
|
|
101
|
+
* If reconnectToStream is waiting, this handles the resume handshake
|
|
102
|
+
* (ACK + stream creation) and returns true. Otherwise returns false
|
|
103
|
+
* so the caller can use its own fallback path.
|
|
104
|
+
*/
|
|
105
|
+
handleStreamResuming(data) {
|
|
106
|
+
if (!this._resumeResolver) return false;
|
|
107
|
+
this._resumeResolver(data);
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Called by onAgentMessage when it receives CF_AGENT_STREAM_RESUME_NONE.
|
|
112
|
+
* If reconnectToStream is waiting, resolves the promise with null
|
|
113
|
+
* immediately (no 5-second timeout). Returns true if handled.
|
|
114
|
+
*/
|
|
115
|
+
handleStreamResumeNone() {
|
|
116
|
+
if (!this._resumeNoneResolver) return false;
|
|
117
|
+
this._resumeNoneResolver();
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Called by onAgentMessage when it receives CF_AGENT_STREAM_PENDING (#1784):
|
|
122
|
+
* the server accepted a turn but its stream has not started yet. If a resume
|
|
123
|
+
* path is awaiting, extend its probe timeout (so it keeps waiting for the
|
|
124
|
+
* eventual STREAM_RESUMING / STREAM_RESUME_NONE instead of resolving null
|
|
125
|
+
* after the short window). Returns true if a waiting path consumed it.
|
|
126
|
+
*/
|
|
127
|
+
handleStreamPending() {
|
|
128
|
+
if (!this._onStreamPending) return false;
|
|
129
|
+
this._onStreamPending();
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Called by the hook's shared message handler when a server turn finishes
|
|
134
|
+
* outside the currently attached transport stream, such as after local-only
|
|
135
|
+
* client cleanup.
|
|
136
|
+
*/
|
|
137
|
+
handleServerTurnCompleted(requestId) {
|
|
138
|
+
this.clearActiveServerTurn(requestId);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Register a server turn that is being rendered outside a transport-owned
|
|
142
|
+
* stream, such as the hook's fallback cross-tab/resume observer path.
|
|
143
|
+
*/
|
|
144
|
+
observeServerTurn(requestId) {
|
|
145
|
+
this.setActiveServerTurn(requestId, null);
|
|
146
|
+
}
|
|
147
|
+
async sendMessages(options) {
|
|
148
|
+
const requestId = nanoid(8);
|
|
149
|
+
const abortController = new AbortController();
|
|
150
|
+
let completed = false;
|
|
151
|
+
let requestSent = false;
|
|
152
|
+
let extraBody = {};
|
|
153
|
+
if (this.prepareBody) extraBody = await this.prepareBody({
|
|
154
|
+
messages: options.messages,
|
|
155
|
+
trigger: options.trigger,
|
|
156
|
+
messageId: options.messageId
|
|
157
|
+
});
|
|
158
|
+
if (options.body) extraBody = {
|
|
159
|
+
...extraBody,
|
|
160
|
+
...options.body
|
|
161
|
+
};
|
|
162
|
+
const bodyPayload = JSON.stringify({
|
|
163
|
+
messages: options.messages,
|
|
164
|
+
trigger: options.trigger,
|
|
165
|
+
...extraBody
|
|
166
|
+
});
|
|
167
|
+
this.activeRequestIds?.add(requestId);
|
|
168
|
+
const agent = this.agent;
|
|
169
|
+
const activeIds = this.activeRequestIds;
|
|
170
|
+
const finish = (action, keepId = false, clearServerTurn = true) => {
|
|
171
|
+
if (completed) return;
|
|
172
|
+
completed = true;
|
|
173
|
+
if (clearServerTurn) this.clearActiveServerTurn(requestId);
|
|
174
|
+
try {
|
|
175
|
+
action();
|
|
176
|
+
} catch {}
|
|
177
|
+
if (!keepId) activeIds?.delete(requestId);
|
|
178
|
+
abortController.abort();
|
|
179
|
+
};
|
|
180
|
+
const abortError = /* @__PURE__ */ new Error("Aborted");
|
|
181
|
+
abortError.name = "AbortError";
|
|
182
|
+
const cancelActiveRequest = () => {
|
|
183
|
+
if (completed) return false;
|
|
184
|
+
finish(() => streamController.error(abortError), true);
|
|
185
|
+
return true;
|
|
186
|
+
};
|
|
187
|
+
this.setActiveServerTurn(requestId, cancelActiveRequest);
|
|
188
|
+
const onAbort = () => {
|
|
189
|
+
if (completed) return;
|
|
190
|
+
if (this.cancelOnClientAbort) {
|
|
191
|
+
if (requestSent) this.sendCancelFrame(requestId);
|
|
192
|
+
finish(() => streamController.error(abortError), requestSent);
|
|
193
|
+
} else finish(() => streamController.error(abortError), false, !requestSent);
|
|
194
|
+
};
|
|
195
|
+
let streamController;
|
|
196
|
+
const stream = new ReadableStream({
|
|
197
|
+
start(controller) {
|
|
198
|
+
streamController = controller;
|
|
199
|
+
const onMessage = (event) => {
|
|
200
|
+
try {
|
|
201
|
+
const data = JSON.parse(event.data);
|
|
202
|
+
if (data.type !== "cf_agent_use_chat_response") return;
|
|
203
|
+
if (data.id !== requestId) return;
|
|
204
|
+
if (data.error) {
|
|
205
|
+
finish(() => controller.error(new Error(data.body || "Stream error")));
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (data.body?.trim()) try {
|
|
209
|
+
const chunk = JSON.parse(data.body);
|
|
210
|
+
controller.enqueue(chunk);
|
|
211
|
+
} catch {}
|
|
212
|
+
if (data.done) finish(() => controller.close());
|
|
213
|
+
} catch {}
|
|
214
|
+
};
|
|
215
|
+
const onClose = () => {
|
|
216
|
+
finish(() => controller.close(), false, false);
|
|
217
|
+
};
|
|
218
|
+
agent.addEventListener("message", onMessage, { signal: abortController.signal });
|
|
219
|
+
agent.addEventListener("close", onClose, { signal: abortController.signal });
|
|
220
|
+
},
|
|
221
|
+
cancel() {
|
|
222
|
+
onAbort();
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
if (options.abortSignal) {
|
|
226
|
+
options.abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
227
|
+
if (options.abortSignal.aborted) onAbort();
|
|
228
|
+
}
|
|
229
|
+
if (completed) return stream;
|
|
230
|
+
requestSent = true;
|
|
231
|
+
agent.send(JSON.stringify({
|
|
232
|
+
id: requestId,
|
|
233
|
+
init: {
|
|
234
|
+
method: "POST",
|
|
235
|
+
body: bodyPayload
|
|
236
|
+
},
|
|
237
|
+
type: "cf_agent_use_chat_request"
|
|
238
|
+
}));
|
|
239
|
+
return stream;
|
|
240
|
+
}
|
|
241
|
+
async reconnectToStream(_options) {
|
|
242
|
+
if (this._expectToolContinuation) {
|
|
243
|
+
this._expectToolContinuation = false;
|
|
244
|
+
return this._createToolContinuationStream();
|
|
245
|
+
}
|
|
246
|
+
const activeIds = this.activeRequestIds;
|
|
247
|
+
return new Promise((resolve) => {
|
|
248
|
+
let resolved = false;
|
|
249
|
+
let timeout;
|
|
250
|
+
const done = (value) => {
|
|
251
|
+
if (resolved) return;
|
|
252
|
+
resolved = true;
|
|
253
|
+
this._resumeResolver = null;
|
|
254
|
+
this._resumeNoneResolver = null;
|
|
255
|
+
this._onStreamPending = null;
|
|
256
|
+
if (timeout) clearTimeout(timeout);
|
|
257
|
+
resolve(value);
|
|
258
|
+
};
|
|
259
|
+
this._onStreamPending = () => {
|
|
260
|
+
if (resolved) return;
|
|
261
|
+
if (timeout) clearTimeout(timeout);
|
|
262
|
+
timeout = setTimeout(() => done(null), RESUME_PENDING_TIMEOUT_MS);
|
|
263
|
+
};
|
|
264
|
+
this._resumeNoneResolver = () => done(null);
|
|
265
|
+
this._resumeResolver = (data) => {
|
|
266
|
+
const requestId = data.id;
|
|
267
|
+
activeIds?.add(requestId);
|
|
268
|
+
const stream = this._createResumeStream(requestId);
|
|
269
|
+
this.agent.send(JSON.stringify({
|
|
270
|
+
type: "cf_agent_stream_resume_ack",
|
|
271
|
+
id: requestId
|
|
272
|
+
}));
|
|
273
|
+
done(stream);
|
|
274
|
+
};
|
|
275
|
+
try {
|
|
276
|
+
this.agent.send(JSON.stringify({ type: "cf_agent_stream_resume_request" }));
|
|
277
|
+
} catch {}
|
|
278
|
+
timeout = setTimeout(() => done(null), RESUME_PROBE_TIMEOUT_MS);
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Creates a deferred ReadableStream for client-side tool continuations.
|
|
283
|
+
* The stream is returned immediately so AI SDK status becomes "submitted"
|
|
284
|
+
* right after addToolOutput()/addToolApprovalResponse(), then it waits for
|
|
285
|
+
* the server to announce the continuation via STREAM_RESUMING.
|
|
286
|
+
*/
|
|
287
|
+
_createToolContinuationStream() {
|
|
288
|
+
const agent = this.agent;
|
|
289
|
+
const activeIds = this.activeRequestIds;
|
|
290
|
+
const streamController = new AbortController();
|
|
291
|
+
const abortError = /* @__PURE__ */ new Error("Aborted");
|
|
292
|
+
abortError.name = "AbortError";
|
|
293
|
+
let completed = false;
|
|
294
|
+
let requestId = null;
|
|
295
|
+
let readerController;
|
|
296
|
+
let onResumeRef = null;
|
|
297
|
+
let onResumeNoneRef = null;
|
|
298
|
+
const clearHandshakeResolvers = (resumeResolver, resumeNoneResolver) => {
|
|
299
|
+
if (resumeResolver === void 0 && resumeNoneResolver === void 0) {
|
|
300
|
+
this._resumeResolver = null;
|
|
301
|
+
this._resumeNoneResolver = null;
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (resumeResolver && this._resumeResolver === resumeResolver) this._resumeResolver = null;
|
|
305
|
+
if (resumeNoneResolver && this._resumeNoneResolver === resumeNoneResolver) this._resumeNoneResolver = null;
|
|
306
|
+
};
|
|
307
|
+
const finish = (action, resumeResolver, resumeNoneResolver, keepRequestId = false) => {
|
|
308
|
+
if (completed) return;
|
|
309
|
+
completed = true;
|
|
310
|
+
this._abortToolContinuation = null;
|
|
311
|
+
this._onStreamPending = null;
|
|
312
|
+
clearHandshakeResolvers(resumeResolver, resumeNoneResolver);
|
|
313
|
+
try {
|
|
314
|
+
action();
|
|
315
|
+
} catch {}
|
|
316
|
+
if (requestId && !keepRequestId) activeIds?.delete(requestId);
|
|
317
|
+
streamController.abort();
|
|
318
|
+
};
|
|
319
|
+
const transport = this;
|
|
320
|
+
this._abortToolContinuation = () => {
|
|
321
|
+
if (completed) return false;
|
|
322
|
+
if (requestId === null) {
|
|
323
|
+
finish(() => readerController.error(abortError), onResumeRef, onResumeNoneRef);
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
try {
|
|
327
|
+
agent.send(JSON.stringify({
|
|
328
|
+
type: "cf_agent_chat_request_cancel",
|
|
329
|
+
id: requestId
|
|
330
|
+
}));
|
|
331
|
+
} catch {}
|
|
332
|
+
finish(() => readerController.error(abortError), onResumeRef, onResumeNoneRef, true);
|
|
333
|
+
return true;
|
|
334
|
+
};
|
|
335
|
+
return new ReadableStream({
|
|
336
|
+
start(controller) {
|
|
337
|
+
readerController = controller;
|
|
338
|
+
let timeout;
|
|
339
|
+
const onResumeNone = () => {
|
|
340
|
+
if (timeout) clearTimeout(timeout);
|
|
341
|
+
finish(() => controller.close(), onResume, onResumeNone);
|
|
342
|
+
};
|
|
343
|
+
const onResume = (data) => {
|
|
344
|
+
if (requestId) return;
|
|
345
|
+
requestId = data.id;
|
|
346
|
+
activeIds?.add(requestId);
|
|
347
|
+
clearHandshakeResolvers(onResume, onResumeNone);
|
|
348
|
+
transport._onStreamPending = null;
|
|
349
|
+
if (timeout) clearTimeout(timeout);
|
|
350
|
+
agent.send(JSON.stringify({
|
|
351
|
+
type: "cf_agent_stream_resume_ack",
|
|
352
|
+
id: requestId
|
|
353
|
+
}));
|
|
354
|
+
};
|
|
355
|
+
onResumeRef = onResume;
|
|
356
|
+
onResumeNoneRef = onResumeNone;
|
|
357
|
+
timeout = setTimeout(() => finish(() => controller.close(), onResume, onResumeNone), RESUME_PROBE_TIMEOUT_MS);
|
|
358
|
+
transport._onStreamPending = () => {
|
|
359
|
+
if (completed) return;
|
|
360
|
+
if (timeout) clearTimeout(timeout);
|
|
361
|
+
timeout = setTimeout(() => finish(() => controller.close(), onResume, onResumeNone), RESUME_PENDING_TIMEOUT_MS);
|
|
362
|
+
};
|
|
363
|
+
transport._resumeResolver = onResume;
|
|
364
|
+
transport._resumeNoneResolver = onResumeNone;
|
|
365
|
+
const onMessage = (event) => {
|
|
366
|
+
try {
|
|
367
|
+
const data = JSON.parse(event.data);
|
|
368
|
+
if (data.type !== "cf_agent_use_chat_response" || requestId == null || data.id !== requestId) return;
|
|
369
|
+
if (data.error) {
|
|
370
|
+
finish(() => controller.error(new Error(data.body || "Stream error")), onResume, onResumeNone);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
if (data.body?.trim()) try {
|
|
374
|
+
const chunk = JSON.parse(data.body);
|
|
375
|
+
controller.enqueue(chunk);
|
|
376
|
+
} catch {}
|
|
377
|
+
if (data.done) finish(() => controller.close(), onResume, onResumeNone);
|
|
378
|
+
} catch {}
|
|
379
|
+
};
|
|
380
|
+
const onClose = () => {
|
|
381
|
+
if (timeout) clearTimeout(timeout);
|
|
382
|
+
finish(() => controller.close(), onResume, onResumeNone);
|
|
383
|
+
};
|
|
384
|
+
agent.addEventListener("message", onMessage, { signal: streamController.signal });
|
|
385
|
+
agent.addEventListener("close", onClose, { signal: streamController.signal });
|
|
386
|
+
try {
|
|
387
|
+
agent.send(JSON.stringify({ type: "cf_agent_stream_resume_request" }));
|
|
388
|
+
} catch {
|
|
389
|
+
finish(() => controller.close());
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
cancel() {
|
|
393
|
+
if (requestId && transport.cancelOnClientAbort) {
|
|
394
|
+
transport.sendCancelFrame(requestId);
|
|
395
|
+
finish(() => {}, onResumeRef, onResumeNoneRef, true);
|
|
396
|
+
} else finish(() => {}, onResumeRef, onResumeNoneRef);
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Creates a ReadableStream that receives resumed stream chunks
|
|
402
|
+
* and forwards them to useChat as UIMessageChunk objects.
|
|
403
|
+
*/
|
|
404
|
+
_createResumeStream(requestId) {
|
|
405
|
+
const agent = this.agent;
|
|
406
|
+
const activeIds = this.activeRequestIds;
|
|
407
|
+
const chunkController = new AbortController();
|
|
408
|
+
const abortError = /* @__PURE__ */ new Error("Aborted");
|
|
409
|
+
abortError.name = "AbortError";
|
|
410
|
+
let completed = false;
|
|
411
|
+
const finish = (action, keepId = false, clearServerTurn = true) => {
|
|
412
|
+
if (completed) return;
|
|
413
|
+
completed = true;
|
|
414
|
+
if (clearServerTurn) this.clearActiveServerTurn(requestId);
|
|
415
|
+
try {
|
|
416
|
+
action();
|
|
417
|
+
} catch {}
|
|
418
|
+
if (!keepId) activeIds?.delete(requestId);
|
|
419
|
+
chunkController.abort();
|
|
420
|
+
};
|
|
421
|
+
const cancelActiveRequest = () => {
|
|
422
|
+
if (completed) return false;
|
|
423
|
+
finish(() => streamController.error(abortError), true);
|
|
424
|
+
return true;
|
|
425
|
+
};
|
|
426
|
+
this.setActiveServerTurn(requestId, cancelActiveRequest);
|
|
427
|
+
let streamController;
|
|
428
|
+
const transport = this;
|
|
429
|
+
return new ReadableStream({
|
|
430
|
+
start(controller) {
|
|
431
|
+
streamController = controller;
|
|
432
|
+
const onMessage = (event) => {
|
|
433
|
+
try {
|
|
434
|
+
const data = JSON.parse(event.data);
|
|
435
|
+
if (data.type !== "cf_agent_use_chat_response") return;
|
|
436
|
+
if (data.id !== requestId) return;
|
|
437
|
+
if (data.error) {
|
|
438
|
+
finish(() => controller.error(new Error(data.body || "Stream error")));
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
if (data.body?.trim()) try {
|
|
442
|
+
const chunk = JSON.parse(data.body);
|
|
443
|
+
controller.enqueue(chunk);
|
|
444
|
+
} catch {}
|
|
445
|
+
if (data.done) finish(() => controller.close());
|
|
446
|
+
} catch {}
|
|
447
|
+
};
|
|
448
|
+
const onClose = () => {
|
|
449
|
+
finish(() => controller.close(), false, false);
|
|
450
|
+
};
|
|
451
|
+
agent.addEventListener("message", onMessage, { signal: chunkController.signal });
|
|
452
|
+
agent.addEventListener("close", onClose, { signal: chunkController.signal });
|
|
453
|
+
},
|
|
454
|
+
cancel() {
|
|
455
|
+
if (transport.cancelOnClientAbort) {
|
|
456
|
+
transport.sendCancelFrame(requestId);
|
|
457
|
+
finish(() => {}, true);
|
|
458
|
+
} else finish(() => {}, false, false);
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
//#endregion
|
|
464
|
+
//#region src/chat/react.tsx
|
|
465
|
+
/**
|
|
466
|
+
* One-shot deprecation warnings (warns once per key per session).
|
|
467
|
+
*/
|
|
468
|
+
const _deprecationWarnings = /* @__PURE__ */ new Set();
|
|
469
|
+
function warnDeprecated(id, message) {
|
|
470
|
+
if (!_deprecationWarnings.has(id)) {
|
|
471
|
+
_deprecationWarnings.add(id);
|
|
472
|
+
console.warn(`[agents/chat] Deprecated: ${message}`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Extracts tool schemas from tools that have client-side execute functions.
|
|
477
|
+
* These schemas are automatically sent to the server with each request.
|
|
478
|
+
*
|
|
479
|
+
* Called internally by `useAgentChat` when `tools` are provided.
|
|
480
|
+
* Most apps do not need to call this directly.
|
|
481
|
+
*
|
|
482
|
+
* @param tools - Record of tool name to tool definition
|
|
483
|
+
* @returns Array of tool schemas to send to server, or undefined if none
|
|
484
|
+
*/
|
|
485
|
+
function extractClientToolSchemas(tools) {
|
|
486
|
+
if (!tools) return void 0;
|
|
487
|
+
const schemas = Object.entries(tools).filter(([_, tool]) => tool.execute).map(([name, tool]) => {
|
|
488
|
+
if (tool.inputSchema && !tool.parameters) console.warn(`[useAgentChat] Tool "${name}" uses deprecated 'inputSchema'. Please migrate to 'parameters'.`);
|
|
489
|
+
return {
|
|
490
|
+
name,
|
|
491
|
+
description: tool.description,
|
|
492
|
+
parameters: tool.parameters ?? tool.inputSchema
|
|
493
|
+
};
|
|
494
|
+
});
|
|
495
|
+
return schemas.length > 0 ? schemas : void 0;
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Map internal tool part states to simplified UI-relevant states.
|
|
499
|
+
*
|
|
500
|
+
* @example
|
|
501
|
+
* ```tsx
|
|
502
|
+
* import { isToolUIPart } from "ai";
|
|
503
|
+
* import { getToolPartState } from "@cloudflare/ai-chat/react";
|
|
504
|
+
*
|
|
505
|
+
* if (isToolUIPart(part)) {
|
|
506
|
+
* const state = getToolPartState(part);
|
|
507
|
+
* if (state === "complete") { ... }
|
|
508
|
+
* if (state === "waiting-approval") { ... }
|
|
509
|
+
* }
|
|
510
|
+
* ```
|
|
511
|
+
*/
|
|
512
|
+
function getToolPartState(part) {
|
|
513
|
+
switch (part.state) {
|
|
514
|
+
case "input-streaming": return "streaming";
|
|
515
|
+
case "approval-requested": return "waiting-approval";
|
|
516
|
+
case "approval-responded": return "approved";
|
|
517
|
+
case "output-available": return "complete";
|
|
518
|
+
case "output-error": return "error";
|
|
519
|
+
case "output-denied": return "denied";
|
|
520
|
+
default: return "loading";
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
/** Get the tool call ID from a tool UI part. */
|
|
524
|
+
function getToolCallId(part) {
|
|
525
|
+
return part.toolCallId;
|
|
526
|
+
}
|
|
527
|
+
/** Get the tool input from a tool UI part (if available). */
|
|
528
|
+
function getToolInput(part) {
|
|
529
|
+
return part.input;
|
|
530
|
+
}
|
|
531
|
+
/** Get the tool output from a tool UI part (if available). */
|
|
532
|
+
function getToolOutput(part) {
|
|
533
|
+
return part.output;
|
|
534
|
+
}
|
|
535
|
+
/** Get the approval info from a tool UI part (if in approval state). */
|
|
536
|
+
function getToolApproval(part) {
|
|
537
|
+
return part.approval;
|
|
538
|
+
}
|
|
539
|
+
function agentNameToKebab(name) {
|
|
540
|
+
if (name === name.toUpperCase() && name !== name.toLowerCase()) return name.toLowerCase().replace(/_/g, "-");
|
|
541
|
+
let result = name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
|
|
542
|
+
result = result.startsWith("-") ? result.slice(1) : result;
|
|
543
|
+
return result.replace(/_/g, "-").replace(/-$/, "");
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Fetch messages from an agent's `/get-messages` HTTP endpoint.
|
|
547
|
+
*
|
|
548
|
+
* Use in framework route loaders to prefetch messages before the component
|
|
549
|
+
* tree mounts, or anywhere you need messages outside a React hook.
|
|
550
|
+
*
|
|
551
|
+
* @example Standard routing
|
|
552
|
+
* ```typescript
|
|
553
|
+
* import { getAgentMessages } from "@cloudflare/ai-chat/react";
|
|
554
|
+
*
|
|
555
|
+
* const messages = await getAgentMessages({
|
|
556
|
+
* host: "https://my-app.workers.dev",
|
|
557
|
+
* agent: "ChatAgent",
|
|
558
|
+
* name: "session-123"
|
|
559
|
+
* });
|
|
560
|
+
* ```
|
|
561
|
+
*
|
|
562
|
+
* @example With basePath (custom URL)
|
|
563
|
+
* ```typescript
|
|
564
|
+
* const messages = await getAgentMessages({
|
|
565
|
+
* url: "https://my-app.workers.dev/custom/path/get-messages"
|
|
566
|
+
* });
|
|
567
|
+
* ```
|
|
568
|
+
*/
|
|
569
|
+
async function getAgentMessages(options) {
|
|
570
|
+
let messagesUrl;
|
|
571
|
+
if ("url" in options) messagesUrl = options.url;
|
|
572
|
+
else {
|
|
573
|
+
const agentSlug = agentNameToKebab(options.agent);
|
|
574
|
+
messagesUrl = `${options.host.endsWith("/") ? options.host.slice(0, -1) : options.host}/agents/${agentSlug}/${options.name}/get-messages`;
|
|
575
|
+
}
|
|
576
|
+
try {
|
|
577
|
+
const response = await fetch(messagesUrl, {
|
|
578
|
+
credentials: options.credentials,
|
|
579
|
+
headers: options.headers
|
|
580
|
+
});
|
|
581
|
+
if (!response.ok) {
|
|
582
|
+
console.warn(`[getAgentMessages] Failed to fetch: ${response.status} ${response.statusText}`);
|
|
583
|
+
return [];
|
|
584
|
+
}
|
|
585
|
+
const text = await response.text();
|
|
586
|
+
if (!text.trim()) return [];
|
|
587
|
+
return JSON.parse(text);
|
|
588
|
+
} catch (error) {
|
|
589
|
+
console.warn("[getAgentMessages] Fetch error:", error);
|
|
590
|
+
return [];
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Module-level cache for initial message fetches. Intentionally shared across
|
|
595
|
+
* all useAgentChat instances to deduplicate requests during React Strict Mode
|
|
596
|
+
* double-renders and re-renders. Cache keys include the agent URL, agent type,
|
|
597
|
+
* and thread name to prevent cross-agent collisions.
|
|
598
|
+
*/
|
|
599
|
+
const requestCache = /* @__PURE__ */ new Map();
|
|
600
|
+
function findLastAssistantMessage(messages) {
|
|
601
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
602
|
+
const message = messages[index];
|
|
603
|
+
if (message.role === "assistant") return {
|
|
604
|
+
index,
|
|
605
|
+
message
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
function moveMessageToEnd(messages, messageId) {
|
|
611
|
+
const idx = messages.findIndex((m) => m.id === messageId);
|
|
612
|
+
if (idx < 0 || idx === messages.length - 1) return messages;
|
|
613
|
+
const result = [...messages];
|
|
614
|
+
const [msg] = result.splice(idx, 1);
|
|
615
|
+
if (!msg) return messages;
|
|
616
|
+
result.push(msg);
|
|
617
|
+
return result;
|
|
618
|
+
}
|
|
619
|
+
function prependMissingHydratedMessages(hydratedMessages, currentMessages) {
|
|
620
|
+
if (currentMessages.length === 0) return hydratedMessages;
|
|
621
|
+
const currentMessageIds = new Set(currentMessages.map((message) => message.id));
|
|
622
|
+
const missingHydratedMessages = hydratedMessages.filter((message) => !currentMessageIds.has(message.id));
|
|
623
|
+
if (missingHydratedMessages.length === 0) return currentMessages;
|
|
624
|
+
return [...missingHydratedMessages, ...currentMessages];
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* React hook for building AI chat interfaces using an Agent
|
|
628
|
+
* @param options Chat options including the agent connection
|
|
629
|
+
* @returns Chat interface controls and state with added clearHistory method
|
|
630
|
+
*/
|
|
631
|
+
/**
|
|
632
|
+
* Automatically detects which tools require confirmation based on their configuration.
|
|
633
|
+
* Tools require confirmation if they have no execute function AND are not server-executed.
|
|
634
|
+
* @param tools - Record of tool name to tool definition
|
|
635
|
+
* @returns Array of tool names that require confirmation
|
|
636
|
+
*
|
|
637
|
+
* @deprecated Use `needsApproval` on server-side tools instead.
|
|
638
|
+
*/
|
|
639
|
+
function detectToolsRequiringConfirmation(tools) {
|
|
640
|
+
warnDeprecated("detectToolsRequiringConfirmation", "detectToolsRequiringConfirmation() is deprecated. Use needsApproval on server-side tools instead. Will be removed in the next major version.");
|
|
641
|
+
if (!tools) return [];
|
|
642
|
+
return Object.entries(tools).filter(([_name, tool]) => !tool.execute).map(([name]) => name);
|
|
643
|
+
}
|
|
644
|
+
function useAgentChat(options) {
|
|
645
|
+
const { agent, getInitialMessages, messages: optionsInitialMessages, onToolCall, onData, experimental_automaticToolResolution, tools, toolsRequiringConfirmation: manualToolsRequiringConfirmation, autoContinueAfterToolResult = true, autoSendAfterAllConfirmationsResolved = true, resume = true, cancelOnClientAbort = false, syncMessagesToServer = true, body: bodyOption, prepareSendMessagesRequest, ...rest } = options;
|
|
646
|
+
if (manualToolsRequiringConfirmation) warnDeprecated("useAgentChat.toolsRequiringConfirmation", "The 'toolsRequiringConfirmation' option is deprecated. Use needsApproval on server-side tools instead. Will be removed in the next major version.");
|
|
647
|
+
if (experimental_automaticToolResolution) warnDeprecated("useAgentChat.experimental_automaticToolResolution", "The 'experimental_automaticToolResolution' option is deprecated. Use the onToolCall callback instead. Will be removed in the next major version.");
|
|
648
|
+
if (options.autoSendAfterAllConfirmationsResolved !== void 0) warnDeprecated("useAgentChat.autoSendAfterAllConfirmationsResolved", "The 'autoSendAfterAllConfirmationsResolved' option is deprecated. Use sendAutomaticallyWhen from AI SDK instead. Will be removed in the next major version.");
|
|
649
|
+
const toolsRequiringConfirmation = useMemo(() => {
|
|
650
|
+
if (manualToolsRequiringConfirmation) return manualToolsRequiringConfirmation;
|
|
651
|
+
if (!tools) return [];
|
|
652
|
+
return Object.entries(tools).filter(([_name, tool]) => !tool.execute).map(([name]) => name);
|
|
653
|
+
}, [manualToolsRequiringConfirmation, tools]);
|
|
654
|
+
const onToolCallRef = useRef(onToolCall);
|
|
655
|
+
onToolCallRef.current = onToolCall;
|
|
656
|
+
const onDataRef = useRef(onData);
|
|
657
|
+
onDataRef.current = onData;
|
|
658
|
+
const rawHttpUrl = agent.getHttpUrl();
|
|
659
|
+
const agentUrl = rawHttpUrl ? new URL(rawHttpUrl) : null;
|
|
660
|
+
if (agentUrl) agentUrl.searchParams.delete("_pk");
|
|
661
|
+
const agentUrlString = agentUrl?.toString() ?? null;
|
|
662
|
+
const agentAddressKey = Array.isArray(agent.path) ? JSON.stringify(agent.path.map((step) => [step.agent, step.name])) : JSON.stringify([[agent.agent ?? "", agent.name ?? ""]]);
|
|
663
|
+
const resolvedInitialMessagesCacheKey = agentUrl ? `${agentUrl.origin}${agentUrl.pathname}|${agentAddressKey}` : null;
|
|
664
|
+
const initialMessagesCacheKey = agentAddressKey;
|
|
665
|
+
const stableChatIdRef = useRef(null);
|
|
666
|
+
const previousAgentRef = useRef(null);
|
|
667
|
+
const previousAgentAddressKeyRef = useRef(null);
|
|
668
|
+
const fallbackChatId = agentAddressKey;
|
|
669
|
+
const agentPathChanged = Array.isArray(agent.path) && previousAgentAddressKeyRef.current !== null && previousAgentAddressKeyRef.current !== agentAddressKey;
|
|
670
|
+
if (stableChatIdRef.current === null) stableChatIdRef.current = resolvedInitialMessagesCacheKey ?? fallbackChatId;
|
|
671
|
+
else if (previousAgentRef.current !== agent || agentPathChanged) stableChatIdRef.current = resolvedInitialMessagesCacheKey ?? fallbackChatId;
|
|
672
|
+
previousAgentRef.current = agent;
|
|
673
|
+
previousAgentAddressKeyRef.current = agentAddressKey;
|
|
674
|
+
const agentRef = useRef(agent);
|
|
675
|
+
agentRef.current = agent;
|
|
676
|
+
async function defaultGetInitialMessagesFetch({ url }) {
|
|
677
|
+
if (!url) return [];
|
|
678
|
+
const getMessagesUrl = new URL(url);
|
|
679
|
+
getMessagesUrl.pathname += "/get-messages";
|
|
680
|
+
const response = await fetch(getMessagesUrl.toString(), {
|
|
681
|
+
credentials: options.credentials,
|
|
682
|
+
headers: options.headers
|
|
683
|
+
});
|
|
684
|
+
if (!response.ok) {
|
|
685
|
+
console.warn(`Failed to fetch initial messages: ${response.status} ${response.statusText}`);
|
|
686
|
+
return [];
|
|
687
|
+
}
|
|
688
|
+
const text = await response.text();
|
|
689
|
+
if (!text.trim()) return [];
|
|
690
|
+
try {
|
|
691
|
+
return JSON.parse(text);
|
|
692
|
+
} catch (error) {
|
|
693
|
+
console.warn("Failed to parse initial messages JSON:", error);
|
|
694
|
+
return [];
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
const getInitialMessagesFetch = getInitialMessages || defaultGetInitialMessagesFetch;
|
|
698
|
+
function doGetInitialMessages(getInitialMessagesOptions, cacheKey) {
|
|
699
|
+
if (requestCache.has(cacheKey)) return requestCache.get(cacheKey);
|
|
700
|
+
const promise = getInitialMessagesFetch(getInitialMessagesOptions);
|
|
701
|
+
requestCache.set(cacheKey, promise);
|
|
702
|
+
return promise;
|
|
703
|
+
}
|
|
704
|
+
const initialMessagesPromise = !(getInitialMessages === null ? false : getInitialMessages ? true : !!agentUrlString) ? null : doGetInitialMessages({
|
|
705
|
+
agent: agent.agent,
|
|
706
|
+
name: agent.name,
|
|
707
|
+
url: agentUrlString ?? void 0
|
|
708
|
+
}, initialMessagesCacheKey);
|
|
709
|
+
const initialMessages = initialMessagesPromise ? use(initialMessagesPromise) : optionsInitialMessages ?? [];
|
|
710
|
+
useEffect(() => {
|
|
711
|
+
if (!initialMessagesPromise) return;
|
|
712
|
+
requestCache.set(initialMessagesCacheKey, initialMessagesPromise);
|
|
713
|
+
return () => {
|
|
714
|
+
if (requestCache.get(initialMessagesCacheKey) === initialMessagesPromise) requestCache.delete(initialMessagesCacheKey);
|
|
715
|
+
};
|
|
716
|
+
}, [initialMessagesCacheKey, initialMessagesPromise]);
|
|
717
|
+
const toolsRef = useRef(tools);
|
|
718
|
+
toolsRef.current = tools;
|
|
719
|
+
const prepareSendMessagesRequestRef = useRef(prepareSendMessagesRequest);
|
|
720
|
+
prepareSendMessagesRequestRef.current = prepareSendMessagesRequest;
|
|
721
|
+
const bodyOptionRef = useRef(bodyOption);
|
|
722
|
+
bodyOptionRef.current = bodyOption;
|
|
723
|
+
/**
|
|
724
|
+
* Tracks request IDs initiated by this tab via the transport.
|
|
725
|
+
* Used by onAgentMessage to skip messages already handled by the transport.
|
|
726
|
+
*/
|
|
727
|
+
const localRequestIdsRef = useRef(/* @__PURE__ */ new Set());
|
|
728
|
+
const pendingReplayResumeRequestIdsRef = useRef(/* @__PURE__ */ new Set());
|
|
729
|
+
const replayHydratedAssistantMessageIdsRef = useRef(/* @__PURE__ */ new Set());
|
|
730
|
+
/**
|
|
731
|
+
* Request ids this socket already ACKed via the fallback resume path.
|
|
732
|
+
* The server sends CF_AGENT_STREAM_RESUMING for the same request from
|
|
733
|
+
* both onConnect and its CF_AGENT_STREAM_RESUME_REQUEST handler (#1733).
|
|
734
|
+
* The transport-handled path dedupes the second notify via
|
|
735
|
+
* localRequestIdsRef, but the fallback path used to ACK both — triggering
|
|
736
|
+
* a second full-buffer replay that duplicated streamed parts. Entries are
|
|
737
|
+
* dropped when the turn completes; the whole set resets when the socket
|
|
738
|
+
* closes, since a new connection legitimately needs a fresh ACK+replay.
|
|
739
|
+
*/
|
|
740
|
+
const fallbackAckedResumeRequestIdsRef = useRef(/* @__PURE__ */ new Set());
|
|
741
|
+
const customTransportRef = useRef(null);
|
|
742
|
+
if (customTransportRef.current === null) customTransportRef.current = new WebSocketChatTransport({
|
|
743
|
+
agent: agentRef.current,
|
|
744
|
+
activeRequestIds: localRequestIdsRef.current,
|
|
745
|
+
cancelOnClientAbort,
|
|
746
|
+
prepareBody: async ({ messages: msgs, trigger, messageId }) => {
|
|
747
|
+
let extraBody = {};
|
|
748
|
+
const currentBody = bodyOptionRef.current;
|
|
749
|
+
if (currentBody) extraBody = { ...typeof currentBody === "function" ? await currentBody() : currentBody };
|
|
750
|
+
if (toolsRef.current) {
|
|
751
|
+
const clientToolSchemas = extractClientToolSchemas(toolsRef.current);
|
|
752
|
+
if (clientToolSchemas) extraBody.clientTools = clientToolSchemas;
|
|
753
|
+
}
|
|
754
|
+
if (prepareSendMessagesRequestRef.current) {
|
|
755
|
+
const userResult = await prepareSendMessagesRequestRef.current({
|
|
756
|
+
id: agentRef.current._pk,
|
|
757
|
+
messages: msgs,
|
|
758
|
+
trigger,
|
|
759
|
+
messageId
|
|
760
|
+
});
|
|
761
|
+
if (userResult.body) Object.assign(extraBody, userResult.body);
|
|
762
|
+
}
|
|
763
|
+
return extraBody;
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
customTransportRef.current.agent = agentRef.current;
|
|
767
|
+
customTransportRef.current.setCancelOnClientAbort(cancelOnClientAbort);
|
|
768
|
+
const customTransport = customTransportRef.current;
|
|
769
|
+
const useChatHelpers = useChat({
|
|
770
|
+
...rest,
|
|
771
|
+
onData,
|
|
772
|
+
messages: initialMessages,
|
|
773
|
+
transport: customTransport,
|
|
774
|
+
id: stableChatIdRef.current,
|
|
775
|
+
resume
|
|
776
|
+
});
|
|
777
|
+
const { messages: chatMessages, setMessages, addToolResult, addToolApprovalResponse, sendMessage, resumeStream, status, stop } = useChatHelpers;
|
|
778
|
+
const statusRef = useRef(status);
|
|
779
|
+
statusRef.current = status;
|
|
780
|
+
const resumeStreamRef = useRef(resumeStream);
|
|
781
|
+
resumeStreamRef.current = resumeStream;
|
|
782
|
+
const hasConnectedOnceRef = useRef(false);
|
|
783
|
+
const resumingToolContinuationRef = useRef(false);
|
|
784
|
+
const pendingToolContinuationRef = useRef(false);
|
|
785
|
+
const observedToolContinuationRequestIdRef = useRef(null);
|
|
786
|
+
const continuationLaunchTimerRef = useRef(null);
|
|
787
|
+
const continuationGenerationRef = useRef(0);
|
|
788
|
+
const [isToolContinuation, setIsToolContinuation] = useState(false);
|
|
789
|
+
const resetToolContinuation = useCallback(() => {
|
|
790
|
+
continuationGenerationRef.current++;
|
|
791
|
+
pendingToolContinuationRef.current = false;
|
|
792
|
+
resumingToolContinuationRef.current = false;
|
|
793
|
+
observedToolContinuationRequestIdRef.current = null;
|
|
794
|
+
if (continuationLaunchTimerRef.current) {
|
|
795
|
+
clearTimeout(continuationLaunchTimerRef.current);
|
|
796
|
+
continuationLaunchTimerRef.current = null;
|
|
797
|
+
}
|
|
798
|
+
setIsToolContinuation(false);
|
|
799
|
+
}, []);
|
|
800
|
+
const scheduleToolContinuationLaunch = useCallback(() => {
|
|
801
|
+
if (!pendingToolContinuationRef.current || statusRef.current !== "ready" || continuationLaunchTimerRef.current) return;
|
|
802
|
+
continuationLaunchTimerRef.current = setTimeout(() => {
|
|
803
|
+
continuationLaunchTimerRef.current = null;
|
|
804
|
+
if (!pendingToolContinuationRef.current || statusRef.current !== "ready") return;
|
|
805
|
+
pendingToolContinuationRef.current = false;
|
|
806
|
+
const myGeneration = continuationGenerationRef.current;
|
|
807
|
+
customTransport.expectToolContinuation();
|
|
808
|
+
resumeStream().catch((error) => {
|
|
809
|
+
console.error("[useAgentChat] Tool continuation resume failed:", error);
|
|
810
|
+
}).finally(() => {
|
|
811
|
+
if (continuationGenerationRef.current !== myGeneration) return;
|
|
812
|
+
resumingToolContinuationRef.current = false;
|
|
813
|
+
setIsToolContinuation(false);
|
|
814
|
+
});
|
|
815
|
+
}, 0);
|
|
816
|
+
}, [customTransport, resumeStream]);
|
|
817
|
+
const startToolContinuation = useCallback(() => {
|
|
818
|
+
if (!autoContinueAfterToolResult || resumingToolContinuationRef.current) return;
|
|
819
|
+
++continuationGenerationRef.current;
|
|
820
|
+
resumingToolContinuationRef.current = true;
|
|
821
|
+
pendingToolContinuationRef.current = true;
|
|
822
|
+
setIsToolContinuation(true);
|
|
823
|
+
scheduleToolContinuationLaunch();
|
|
824
|
+
}, [autoContinueAfterToolResult, scheduleToolContinuationLaunch]);
|
|
825
|
+
useEffect(() => {
|
|
826
|
+
if (status === "error" && pendingToolContinuationRef.current) {
|
|
827
|
+
resetToolContinuation();
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
scheduleToolContinuationLaunch();
|
|
831
|
+
}, [
|
|
832
|
+
resetToolContinuation,
|
|
833
|
+
scheduleToolContinuationLaunch,
|
|
834
|
+
status
|
|
835
|
+
]);
|
|
836
|
+
const stopWithToolContinuationAbort = useCallback(async () => {
|
|
837
|
+
try {
|
|
838
|
+
customTransport.cancelActiveServerTurn();
|
|
839
|
+
await stop();
|
|
840
|
+
} finally {
|
|
841
|
+
customTransport.abortActiveToolContinuation();
|
|
842
|
+
}
|
|
843
|
+
}, [stop, customTransport]);
|
|
844
|
+
const processedToolCalls = useRef(/* @__PURE__ */ new Set());
|
|
845
|
+
const isResolvingToolsRef = useRef(false);
|
|
846
|
+
const [toolResolutionTrigger, setToolResolutionTrigger] = useState(0);
|
|
847
|
+
const [clientToolResults, setClientToolResults] = useState(/* @__PURE__ */ new Map());
|
|
848
|
+
const messagesRef = useRef(chatMessages);
|
|
849
|
+
messagesRef.current = chatMessages;
|
|
850
|
+
const initialMessagesRef = useRef(initialMessages);
|
|
851
|
+
initialMessagesRef.current = initialMessages;
|
|
852
|
+
const seededInitialMessagesKeyRef = useRef(null);
|
|
853
|
+
const markInitialMessagesSeeded = useCallback(() => {
|
|
854
|
+
seededInitialMessagesKeyRef.current = initialMessagesCacheKey;
|
|
855
|
+
}, [initialMessagesCacheKey]);
|
|
856
|
+
useEffect(() => {
|
|
857
|
+
if (!initialMessagesPromise) return;
|
|
858
|
+
if (seededInitialMessagesKeyRef.current === initialMessagesCacheKey) return;
|
|
859
|
+
markInitialMessagesSeeded();
|
|
860
|
+
setMessages((prevMessages) => prependMissingHydratedMessages(initialMessagesRef.current, prevMessages));
|
|
861
|
+
}, [
|
|
862
|
+
initialMessagesCacheKey,
|
|
863
|
+
initialMessagesPromise,
|
|
864
|
+
markInitialMessagesSeeded,
|
|
865
|
+
setMessages
|
|
866
|
+
]);
|
|
867
|
+
const localResponseMessageIdsRef = useRef(/* @__PURE__ */ new Map());
|
|
868
|
+
const protectedStreamingAssistantRef = useRef(null);
|
|
869
|
+
const preserveProtectedStreamingAssistant = useCallback((messages) => {
|
|
870
|
+
const protection = protectedStreamingAssistantRef.current;
|
|
871
|
+
if (!protection) return [...messages];
|
|
872
|
+
const protectedIndex = messages.findIndex((message) => message.id === protection.assistantId);
|
|
873
|
+
if (protectedIndex >= 0 && messages.slice(protectedIndex + 1).some((message) => message.role === "assistant")) {
|
|
874
|
+
protectedStreamingAssistantRef.current = null;
|
|
875
|
+
return [...messages];
|
|
876
|
+
}
|
|
877
|
+
const protectedAssistant = messagesRef.current.find((message) => message.id === protection.assistantId) ?? messages.find((message) => message.id === protection.assistantId);
|
|
878
|
+
if (!protectedAssistant) return [...messages];
|
|
879
|
+
return [...messages.filter((message) => message.id !== protection.assistantId), protectedAssistant];
|
|
880
|
+
}, []);
|
|
881
|
+
const protectStreamingAssistantTail = useCallback(() => {
|
|
882
|
+
if (statusRef.current !== "streaming") return;
|
|
883
|
+
const assistantInfo = findLastAssistantMessage(messagesRef.current);
|
|
884
|
+
if (!assistantInfo) return;
|
|
885
|
+
if (protectedStreamingAssistantRef.current?.assistantId !== assistantInfo.message.id) protectedStreamingAssistantRef.current = {
|
|
886
|
+
assistantId: assistantInfo.message.id,
|
|
887
|
+
anchorMessageId: messagesRef.current[assistantInfo.index - 1]?.id ?? null
|
|
888
|
+
};
|
|
889
|
+
setMessages((prevMessages) => {
|
|
890
|
+
const protection = protectedStreamingAssistantRef.current;
|
|
891
|
+
if (!protection) return prevMessages;
|
|
892
|
+
return moveMessageToEnd(prevMessages, protection.assistantId);
|
|
893
|
+
});
|
|
894
|
+
}, [setMessages]);
|
|
895
|
+
const restoreProtectedStreamingAssistant = useCallback((assistantId) => {
|
|
896
|
+
const protection = protectedStreamingAssistantRef.current;
|
|
897
|
+
if (!protection || assistantId !== void 0 && protection.assistantId !== assistantId) return;
|
|
898
|
+
protectedStreamingAssistantRef.current = null;
|
|
899
|
+
setMessages((prevMessages) => {
|
|
900
|
+
const sourceIdx = prevMessages.findIndex((m) => m.id === protection.assistantId);
|
|
901
|
+
if (sourceIdx < 0) return prevMessages;
|
|
902
|
+
const result = [...prevMessages];
|
|
903
|
+
const [msg] = result.splice(sourceIdx, 1);
|
|
904
|
+
if (!msg) return prevMessages;
|
|
905
|
+
if (protection.anchorMessageId === null) result.unshift(msg);
|
|
906
|
+
else {
|
|
907
|
+
const anchorIdx = result.findIndex((m) => m.id === protection.anchorMessageId);
|
|
908
|
+
result.splice(anchorIdx >= 0 ? anchorIdx + 1 : sourceIdx, 0, msg);
|
|
909
|
+
}
|
|
910
|
+
return result;
|
|
911
|
+
});
|
|
912
|
+
}, [setMessages]);
|
|
913
|
+
const resetMatchingHydratedAssistantForReplay = useCallback((messageId) => {
|
|
914
|
+
setMessages((prevMessages) => {
|
|
915
|
+
const lastMessage = prevMessages[prevMessages.length - 1];
|
|
916
|
+
if (!lastMessage || lastMessage.role !== "assistant" || lastMessage.id !== messageId) return prevMessages;
|
|
917
|
+
replayHydratedAssistantMessageIdsRef.current.add(messageId);
|
|
918
|
+
const next = [...prevMessages];
|
|
919
|
+
next[next.length - 1] = {
|
|
920
|
+
...lastMessage,
|
|
921
|
+
parts: []
|
|
922
|
+
};
|
|
923
|
+
return next;
|
|
924
|
+
});
|
|
925
|
+
}, [setMessages]);
|
|
926
|
+
const collapseHydratedReplayTextParts = useCallback((message) => {
|
|
927
|
+
const parts = message.parts;
|
|
928
|
+
const nextParts = parts.filter((part, index) => {
|
|
929
|
+
if (part.type !== "text" || !("text" in part) || !part.text) return true;
|
|
930
|
+
return !parts.some((candidate, candidateIndex) => {
|
|
931
|
+
if (candidateIndex <= index) return false;
|
|
932
|
+
if (candidate.type !== "text" || !("text" in candidate) || !candidate.text) return false;
|
|
933
|
+
return candidate.text.startsWith(part.text);
|
|
934
|
+
});
|
|
935
|
+
});
|
|
936
|
+
return nextParts.length === parts.length ? message : {
|
|
937
|
+
...message,
|
|
938
|
+
parts: nextParts
|
|
939
|
+
};
|
|
940
|
+
}, []);
|
|
941
|
+
useEffect(() => {
|
|
942
|
+
if (replayHydratedAssistantMessageIdsRef.current.size === 0) return;
|
|
943
|
+
const idsToCollapse = new Set(chatMessages.filter((message) => replayHydratedAssistantMessageIdsRef.current.has(message.id) && message.role === "assistant" && collapseHydratedReplayTextParts(message) !== message).map((message) => message.id));
|
|
944
|
+
if (idsToCollapse.size === 0) return;
|
|
945
|
+
setMessages((prevMessages) => {
|
|
946
|
+
let changed = false;
|
|
947
|
+
const nextMessages = prevMessages.map((message) => {
|
|
948
|
+
if (!idsToCollapse.has(message.id)) return message;
|
|
949
|
+
const nextMessage = collapseHydratedReplayTextParts(message);
|
|
950
|
+
if (nextMessage !== message) changed = true;
|
|
951
|
+
return nextMessage;
|
|
952
|
+
});
|
|
953
|
+
return changed ? nextMessages : prevMessages;
|
|
954
|
+
});
|
|
955
|
+
}, [
|
|
956
|
+
chatMessages,
|
|
957
|
+
collapseHydratedReplayTextParts,
|
|
958
|
+
setMessages
|
|
959
|
+
]);
|
|
960
|
+
const resetLocalChatState = useCallback(() => {
|
|
961
|
+
markInitialMessagesSeeded();
|
|
962
|
+
setMessages([]);
|
|
963
|
+
setClientToolResults(/* @__PURE__ */ new Map());
|
|
964
|
+
setPendingOnToolCallIds(/* @__PURE__ */ new Set());
|
|
965
|
+
resetToolContinuation();
|
|
966
|
+
processedToolCalls.current.clear();
|
|
967
|
+
localResponseMessageIdsRef.current.clear();
|
|
968
|
+
pendingReplayResumeRequestIdsRef.current.clear();
|
|
969
|
+
fallbackAckedResumeRequestIdsRef.current.clear();
|
|
970
|
+
replayHydratedAssistantMessageIdsRef.current.clear();
|
|
971
|
+
protectedStreamingAssistantRef.current = null;
|
|
972
|
+
}, [
|
|
973
|
+
markInitialMessagesSeeded,
|
|
974
|
+
setMessages,
|
|
975
|
+
resetToolContinuation
|
|
976
|
+
]);
|
|
977
|
+
const sendMessageWithStreamingProtection = useCallback(async (message, options) => {
|
|
978
|
+
const request = sendMessage(message, options);
|
|
979
|
+
if (message !== void 0 && !(typeof message === "object" && message !== null && "messageId" in message && message.messageId != null)) protectStreamingAssistantTail();
|
|
980
|
+
return request;
|
|
981
|
+
}, [sendMessage, protectStreamingAssistantTail]);
|
|
982
|
+
const lastMessage = chatMessages[chatMessages.length - 1];
|
|
983
|
+
const pendingConfirmations = (() => {
|
|
984
|
+
if (!lastMessage || lastMessage.role !== "assistant") return {
|
|
985
|
+
messageId: void 0,
|
|
986
|
+
toolCallIds: /* @__PURE__ */ new Set()
|
|
987
|
+
};
|
|
988
|
+
const pendingIds = /* @__PURE__ */ new Set();
|
|
989
|
+
for (const part of lastMessage.parts ?? []) if (isToolUIPart(part) && part.state === "input-available" && toolsRequiringConfirmation.includes(getToolName(part))) pendingIds.add(part.toolCallId);
|
|
990
|
+
return {
|
|
991
|
+
messageId: lastMessage.id,
|
|
992
|
+
toolCallIds: pendingIds
|
|
993
|
+
};
|
|
994
|
+
})();
|
|
995
|
+
const pendingConfirmationsRef = useRef(pendingConfirmations);
|
|
996
|
+
pendingConfirmationsRef.current = pendingConfirmations;
|
|
997
|
+
const [pendingOnToolCallIds, setPendingOnToolCallIds] = useState(() => /* @__PURE__ */ new Set());
|
|
998
|
+
const finishOnToolCall = useCallback((toolCallId) => {
|
|
999
|
+
setPendingOnToolCallIds((prev) => {
|
|
1000
|
+
if (!prev.has(toolCallId)) return prev;
|
|
1001
|
+
const next = new Set(prev);
|
|
1002
|
+
next.delete(toolCallId);
|
|
1003
|
+
return next;
|
|
1004
|
+
});
|
|
1005
|
+
}, []);
|
|
1006
|
+
useEffect(() => {
|
|
1007
|
+
if (!experimental_automaticToolResolution) return;
|
|
1008
|
+
if (isResolvingToolsRef.current) return;
|
|
1009
|
+
const lastMsg = chatMessages[chatMessages.length - 1];
|
|
1010
|
+
if (!lastMsg || lastMsg.role !== "assistant") return;
|
|
1011
|
+
const toolCalls = lastMsg.parts.filter((part) => isToolUIPart(part) && part.state === "input-available" && !processedToolCalls.current.has(part.toolCallId));
|
|
1012
|
+
if (toolCalls.length > 0) {
|
|
1013
|
+
const currentTools = toolsRef.current;
|
|
1014
|
+
const toolCallsToResolve = toolCalls.filter((part) => isToolUIPart(part) && !toolsRequiringConfirmation.includes(getToolName(part)) && currentTools?.[getToolName(part)]?.execute);
|
|
1015
|
+
if (toolCallsToResolve.length > 0) {
|
|
1016
|
+
isResolvingToolsRef.current = true;
|
|
1017
|
+
(async () => {
|
|
1018
|
+
try {
|
|
1019
|
+
const toolResults = [];
|
|
1020
|
+
for (const part of toolCallsToResolve) if (isToolUIPart(part)) {
|
|
1021
|
+
let toolOutput = null;
|
|
1022
|
+
const toolName = getToolName(part);
|
|
1023
|
+
const tool = currentTools?.[toolName];
|
|
1024
|
+
if (tool?.execute && part.input !== void 0) try {
|
|
1025
|
+
toolOutput = await tool.execute(part.input);
|
|
1026
|
+
} catch (error) {
|
|
1027
|
+
toolOutput = `Error executing tool: ${error instanceof Error ? error.message : String(error)}`;
|
|
1028
|
+
}
|
|
1029
|
+
processedToolCalls.current.add(part.toolCallId);
|
|
1030
|
+
toolResults.push({
|
|
1031
|
+
toolCallId: part.toolCallId,
|
|
1032
|
+
toolName,
|
|
1033
|
+
output: toolOutput
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
if (toolResults.length > 0) {
|
|
1037
|
+
const clientToolSchemas = extractClientToolSchemas(currentTools);
|
|
1038
|
+
for (const result of toolResults) agentRef.current.send(JSON.stringify({
|
|
1039
|
+
type: "cf_agent_tool_result",
|
|
1040
|
+
toolCallId: result.toolCallId,
|
|
1041
|
+
toolName: result.toolName,
|
|
1042
|
+
output: result.output,
|
|
1043
|
+
autoContinue: autoContinueAfterToolResult,
|
|
1044
|
+
clientTools: clientToolSchemas
|
|
1045
|
+
}));
|
|
1046
|
+
await Promise.all(toolResults.map((result) => addToolResult({
|
|
1047
|
+
tool: result.toolName,
|
|
1048
|
+
toolCallId: result.toolCallId,
|
|
1049
|
+
output: result.output
|
|
1050
|
+
})));
|
|
1051
|
+
setClientToolResults((prev) => {
|
|
1052
|
+
const newMap = new Map(prev);
|
|
1053
|
+
for (const result of toolResults) newMap.set(result.toolCallId, result.output);
|
|
1054
|
+
return newMap;
|
|
1055
|
+
});
|
|
1056
|
+
startToolContinuation();
|
|
1057
|
+
}
|
|
1058
|
+
} finally {
|
|
1059
|
+
isResolvingToolsRef.current = false;
|
|
1060
|
+
setToolResolutionTrigger((c) => c + 1);
|
|
1061
|
+
}
|
|
1062
|
+
})();
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
}, [
|
|
1066
|
+
chatMessages,
|
|
1067
|
+
experimental_automaticToolResolution,
|
|
1068
|
+
addToolResult,
|
|
1069
|
+
toolsRequiringConfirmation,
|
|
1070
|
+
autoContinueAfterToolResult,
|
|
1071
|
+
startToolContinuation,
|
|
1072
|
+
toolResolutionTrigger
|
|
1073
|
+
]);
|
|
1074
|
+
const sendToolOutputToServer = useCallback((toolCallId, toolName, output, state, errorText) => {
|
|
1075
|
+
const shouldAutoContinue = state === "output-error" ? false : autoContinueAfterToolResult;
|
|
1076
|
+
agentRef.current.send(JSON.stringify({
|
|
1077
|
+
type: "cf_agent_tool_result",
|
|
1078
|
+
toolCallId,
|
|
1079
|
+
toolName,
|
|
1080
|
+
output,
|
|
1081
|
+
...state ? { state } : {},
|
|
1082
|
+
...errorText !== void 0 ? { errorText } : {},
|
|
1083
|
+
autoContinue: shouldAutoContinue,
|
|
1084
|
+
clientTools: toolsRef.current ? extractClientToolSchemas(toolsRef.current) : void 0
|
|
1085
|
+
}));
|
|
1086
|
+
if (state !== "output-error") setClientToolResults((prev) => new Map(prev).set(toolCallId, output));
|
|
1087
|
+
if (shouldAutoContinue) startToolContinuation();
|
|
1088
|
+
}, [autoContinueAfterToolResult, startToolContinuation]);
|
|
1089
|
+
const sendToolApprovalToServer = useCallback((toolCallId, approved) => {
|
|
1090
|
+
agentRef.current.send(JSON.stringify({
|
|
1091
|
+
type: "cf_agent_tool_approval",
|
|
1092
|
+
toolCallId,
|
|
1093
|
+
approved,
|
|
1094
|
+
autoContinue: autoContinueAfterToolResult
|
|
1095
|
+
}));
|
|
1096
|
+
if (autoContinueAfterToolResult) startToolContinuation();
|
|
1097
|
+
}, [autoContinueAfterToolResult, startToolContinuation]);
|
|
1098
|
+
useEffect(() => {
|
|
1099
|
+
const currentOnToolCall = onToolCallRef.current;
|
|
1100
|
+
if (!currentOnToolCall) return;
|
|
1101
|
+
const lastMsg = chatMessages[chatMessages.length - 1];
|
|
1102
|
+
if (!lastMsg || lastMsg.role !== "assistant") return;
|
|
1103
|
+
const pendingToolCalls = lastMsg.parts.filter((part) => isToolUIPart(part) && part.state === "input-available" && !processedToolCalls.current.has(part.toolCallId));
|
|
1104
|
+
for (const part of pendingToolCalls) if (isToolUIPart(part)) {
|
|
1105
|
+
const toolCallId = part.toolCallId;
|
|
1106
|
+
const toolName = getToolName(part);
|
|
1107
|
+
processedToolCalls.current.add(toolCallId);
|
|
1108
|
+
setPendingOnToolCallIds((prev) => {
|
|
1109
|
+
if (prev.has(toolCallId)) return prev;
|
|
1110
|
+
const next = new Set(prev);
|
|
1111
|
+
next.add(toolCallId);
|
|
1112
|
+
return next;
|
|
1113
|
+
});
|
|
1114
|
+
const addToolOutput = (opts) => {
|
|
1115
|
+
sendToolOutputToServer(opts.toolCallId, toolName, opts.output, opts.state, opts.errorText);
|
|
1116
|
+
addToolResult({
|
|
1117
|
+
tool: toolName,
|
|
1118
|
+
toolCallId: opts.toolCallId,
|
|
1119
|
+
output: opts.state === "output-error" ? opts.errorText ?? "Tool execution denied by user" : opts.output
|
|
1120
|
+
});
|
|
1121
|
+
};
|
|
1122
|
+
let result;
|
|
1123
|
+
try {
|
|
1124
|
+
result = currentOnToolCall({
|
|
1125
|
+
toolCall: {
|
|
1126
|
+
toolCallId,
|
|
1127
|
+
toolName,
|
|
1128
|
+
input: part.input
|
|
1129
|
+
},
|
|
1130
|
+
addToolOutput
|
|
1131
|
+
});
|
|
1132
|
+
} catch (error) {
|
|
1133
|
+
finishOnToolCall(toolCallId);
|
|
1134
|
+
throw error;
|
|
1135
|
+
}
|
|
1136
|
+
Promise.resolve(result).finally(() => {
|
|
1137
|
+
finishOnToolCall(toolCallId);
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
}, [
|
|
1141
|
+
chatMessages,
|
|
1142
|
+
sendToolOutputToServer,
|
|
1143
|
+
addToolResult,
|
|
1144
|
+
finishOnToolCall
|
|
1145
|
+
]);
|
|
1146
|
+
const streamStateRef = useRef({ status: "idle" });
|
|
1147
|
+
const [isServerStreaming, setIsServerStreaming] = useState(false);
|
|
1148
|
+
const [isRecovering, setIsRecovering] = useState(false);
|
|
1149
|
+
useEffect(() => {
|
|
1150
|
+
const localResponseIds = localResponseMessageIdsRef.current;
|
|
1151
|
+
/**
|
|
1152
|
+
* Unified message handler that parses JSON once and dispatches based on type.
|
|
1153
|
+
* Avoids duplicate parsing overhead from separate listeners.
|
|
1154
|
+
*/
|
|
1155
|
+
function onAgentMessage(event) {
|
|
1156
|
+
if (typeof event.data !== "string") return;
|
|
1157
|
+
let data;
|
|
1158
|
+
try {
|
|
1159
|
+
data = JSON.parse(event.data);
|
|
1160
|
+
} catch (_error) {
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
switch (data.type) {
|
|
1164
|
+
case "cf_agent_chat_clear":
|
|
1165
|
+
streamStateRef.current = transition(streamStateRef.current, { type: "clear" }).state;
|
|
1166
|
+
setIsServerStreaming(false);
|
|
1167
|
+
setIsRecovering(false);
|
|
1168
|
+
resetLocalChatState();
|
|
1169
|
+
break;
|
|
1170
|
+
case "cf_agent_chat_recovering":
|
|
1171
|
+
setIsRecovering(Boolean(data.recovering));
|
|
1172
|
+
break;
|
|
1173
|
+
case "cf_agent_chat_messages": {
|
|
1174
|
+
let next = preserveProtectedStreamingAssistant(data.messages);
|
|
1175
|
+
const observed = streamStateRef.current;
|
|
1176
|
+
if (observed.status === "observing" && observed.accumulator.parts.length > 0) {
|
|
1177
|
+
const snapshotIdx = next.findIndex((m) => m.id === observed.accumulator.messageId);
|
|
1178
|
+
const snapshotParts = snapshotIdx >= 0 ? next[snapshotIdx].parts.length : 0;
|
|
1179
|
+
if (observed.accumulator.parts.length >= snapshotParts) next = observed.accumulator.mergeInto(next);
|
|
1180
|
+
}
|
|
1181
|
+
setMessages(next);
|
|
1182
|
+
break;
|
|
1183
|
+
}
|
|
1184
|
+
case "cf_agent_message_updated":
|
|
1185
|
+
setMessages((prevMessages) => {
|
|
1186
|
+
const updatedMessage = data.message;
|
|
1187
|
+
let idx = prevMessages.findIndex((m) => m.id === updatedMessage.id);
|
|
1188
|
+
if (idx < 0) {
|
|
1189
|
+
const updatedToolCallIds = new Set(updatedMessage.parts.filter((p) => "toolCallId" in p && p.toolCallId).map((p) => p.toolCallId));
|
|
1190
|
+
if (updatedToolCallIds.size > 0) idx = prevMessages.findIndex((m) => m.parts.some((p) => "toolCallId" in p && updatedToolCallIds.has(p.toolCallId)));
|
|
1191
|
+
}
|
|
1192
|
+
if (idx >= 0) {
|
|
1193
|
+
const updated = [...prevMessages];
|
|
1194
|
+
updated[idx] = {
|
|
1195
|
+
...updatedMessage,
|
|
1196
|
+
id: prevMessages[idx].id
|
|
1197
|
+
};
|
|
1198
|
+
return updated;
|
|
1199
|
+
}
|
|
1200
|
+
return prevMessages;
|
|
1201
|
+
});
|
|
1202
|
+
break;
|
|
1203
|
+
case "cf_agent_stream_resume_none":
|
|
1204
|
+
customTransport.handleStreamResumeNone();
|
|
1205
|
+
break;
|
|
1206
|
+
case "cf_agent_stream_pending":
|
|
1207
|
+
customTransport.handleStreamPending();
|
|
1208
|
+
break;
|
|
1209
|
+
case "cf_agent_stream_resuming": {
|
|
1210
|
+
const isEarlyToolContinuation = resumingToolContinuationRef.current && !customTransport.isAwaitingResume();
|
|
1211
|
+
if (!resume && !customTransport.isAwaitingResume()) {
|
|
1212
|
+
if (!isEarlyToolContinuation) return;
|
|
1213
|
+
}
|
|
1214
|
+
if (!resumingToolContinuationRef.current) pendingReplayResumeRequestIdsRef.current.add(data.id);
|
|
1215
|
+
if (customTransport.handleStreamResuming(data)) return;
|
|
1216
|
+
if (localRequestIdsRef.current.has(data.id)) return;
|
|
1217
|
+
if (fallbackAckedResumeRequestIdsRef.current.has(data.id)) return;
|
|
1218
|
+
if (isEarlyToolContinuation) {
|
|
1219
|
+
pendingToolContinuationRef.current = false;
|
|
1220
|
+
observedToolContinuationRequestIdRef.current = data.id;
|
|
1221
|
+
if (continuationLaunchTimerRef.current) {
|
|
1222
|
+
clearTimeout(continuationLaunchTimerRef.current);
|
|
1223
|
+
continuationLaunchTimerRef.current = null;
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
streamStateRef.current = transition(streamStateRef.current, {
|
|
1227
|
+
type: "resume-fallback",
|
|
1228
|
+
streamId: data.id,
|
|
1229
|
+
messageId: nanoid()
|
|
1230
|
+
}).state;
|
|
1231
|
+
customTransport.observeServerTurn(data.id);
|
|
1232
|
+
setIsServerStreaming(true);
|
|
1233
|
+
setIsRecovering(false);
|
|
1234
|
+
fallbackAckedResumeRequestIdsRef.current.add(data.id);
|
|
1235
|
+
agentRef.current.send(JSON.stringify({
|
|
1236
|
+
type: "cf_agent_stream_resume_ack",
|
|
1237
|
+
id: data.id
|
|
1238
|
+
}));
|
|
1239
|
+
break;
|
|
1240
|
+
}
|
|
1241
|
+
case "cf_agent_use_chat_response": {
|
|
1242
|
+
if (localRequestIdsRef.current.has(data.id)) {
|
|
1243
|
+
if (data.body?.trim()) try {
|
|
1244
|
+
const chunkData = JSON.parse(data.body);
|
|
1245
|
+
if (chunkData.type === "start" && typeof chunkData.messageId === "string") {
|
|
1246
|
+
localResponseIds.set(data.id, chunkData.messageId);
|
|
1247
|
+
if (!data.continuation) {
|
|
1248
|
+
if (protectedStreamingAssistantRef.current?.assistantId !== chunkData.messageId) {
|
|
1249
|
+
const msgs = messagesRef.current;
|
|
1250
|
+
const idx = msgs.findIndex((m) => m.id === chunkData.messageId);
|
|
1251
|
+
const anchorMessageId = idx >= 0 ? msgs[idx - 1]?.id ?? null : msgs[msgs.length - 1]?.id ?? null;
|
|
1252
|
+
protectedStreamingAssistantRef.current = {
|
|
1253
|
+
assistantId: chunkData.messageId,
|
|
1254
|
+
anchorMessageId
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
if (data.replay && !data.continuation && !resumingToolContinuationRef.current && observedToolContinuationRequestIdRef.current !== data.id) {
|
|
1259
|
+
pendingReplayResumeRequestIdsRef.current.delete(data.id);
|
|
1260
|
+
resetMatchingHydratedAssistantForReplay(chunkData.messageId);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
} catch {}
|
|
1264
|
+
if (data.done || data.replayComplete) pendingReplayResumeRequestIdsRef.current.delete(data.id);
|
|
1265
|
+
if (data.done) {
|
|
1266
|
+
if (streamStateRef.current.status === "observing" && streamStateRef.current.streamId === data.id) {
|
|
1267
|
+
streamStateRef.current = { status: "idle" };
|
|
1268
|
+
setIsServerStreaming(false);
|
|
1269
|
+
}
|
|
1270
|
+
customTransport.handleServerTurnCompleted(data.id);
|
|
1271
|
+
restoreProtectedStreamingAssistant(localResponseIds.get(data.id));
|
|
1272
|
+
localResponseIds.delete(data.id);
|
|
1273
|
+
localRequestIdsRef.current.delete(data.id);
|
|
1274
|
+
fallbackAckedResumeRequestIdsRef.current.delete(data.id);
|
|
1275
|
+
}
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
let chunkData;
|
|
1279
|
+
if (data.replay && streamStateRef.current.status !== "observing" && !pendingReplayResumeRequestIdsRef.current.has(data.id)) return;
|
|
1280
|
+
if (data.body?.trim()) try {
|
|
1281
|
+
chunkData = JSON.parse(data.body);
|
|
1282
|
+
if (data.replay && !data.continuation && !resumingToolContinuationRef.current && observedToolContinuationRequestIdRef.current !== data.id && typeof chunkData.messageId === "string" && chunkData.type === "start") {
|
|
1283
|
+
pendingReplayResumeRequestIdsRef.current.delete(data.id);
|
|
1284
|
+
resetMatchingHydratedAssistantForReplay(chunkData.messageId);
|
|
1285
|
+
}
|
|
1286
|
+
if (typeof chunkData.type === "string" && chunkData.type.startsWith("data-") && onDataRef.current) onDataRef.current(chunkData);
|
|
1287
|
+
} catch (parseError) {
|
|
1288
|
+
console.warn("[useAgentChat] Failed to parse stream chunk:", parseError instanceof Error ? parseError.message : parseError, "body:", data.body?.slice(0, 100));
|
|
1289
|
+
}
|
|
1290
|
+
if (data.done || data.replayComplete) pendingReplayResumeRequestIdsRef.current.delete(data.id);
|
|
1291
|
+
if (data.done) {
|
|
1292
|
+
customTransport.handleServerTurnCompleted(data.id);
|
|
1293
|
+
fallbackAckedResumeRequestIdsRef.current.delete(data.id);
|
|
1294
|
+
setIsRecovering(false);
|
|
1295
|
+
}
|
|
1296
|
+
const completedObservedToolContinuation = data.done && observedToolContinuationRequestIdRef.current === data.id;
|
|
1297
|
+
const result = transition(streamStateRef.current, {
|
|
1298
|
+
type: "response",
|
|
1299
|
+
streamId: data.id,
|
|
1300
|
+
messageId: nanoid(),
|
|
1301
|
+
chunkData,
|
|
1302
|
+
done: data.done,
|
|
1303
|
+
error: data.error,
|
|
1304
|
+
replay: data.replay,
|
|
1305
|
+
replayComplete: data.replayComplete,
|
|
1306
|
+
continuation: data.continuation,
|
|
1307
|
+
currentMessages: data.continuation ? messagesRef.current : void 0
|
|
1308
|
+
});
|
|
1309
|
+
streamStateRef.current = result.state;
|
|
1310
|
+
if (result.messagesUpdate) setMessages(result.messagesUpdate);
|
|
1311
|
+
setIsServerStreaming(result.isStreaming);
|
|
1312
|
+
if (completedObservedToolContinuation) resetToolContinuation();
|
|
1313
|
+
break;
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
const fallbackAckedResumeRequestIds = fallbackAckedResumeRequestIdsRef.current;
|
|
1318
|
+
function onAgentClose() {
|
|
1319
|
+
fallbackAckedResumeRequestIds.clear();
|
|
1320
|
+
}
|
|
1321
|
+
function onAgentOpen() {
|
|
1322
|
+
if (!hasConnectedOnceRef.current) {
|
|
1323
|
+
hasConnectedOnceRef.current = true;
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
if (!resume || statusRef.current !== "ready" || resumingToolContinuationRef.current || customTransport.isAwaitingResume()) return;
|
|
1327
|
+
Promise.resolve(resumeStreamRef.current?.()).catch(() => {});
|
|
1328
|
+
}
|
|
1329
|
+
agent.addEventListener("message", onAgentMessage);
|
|
1330
|
+
agent.addEventListener("close", onAgentClose);
|
|
1331
|
+
agent.addEventListener("open", onAgentOpen);
|
|
1332
|
+
return () => {
|
|
1333
|
+
agent.removeEventListener("message", onAgentMessage);
|
|
1334
|
+
agent.removeEventListener("close", onAgentClose);
|
|
1335
|
+
agent.removeEventListener("open", onAgentOpen);
|
|
1336
|
+
fallbackAckedResumeRequestIds.clear();
|
|
1337
|
+
streamStateRef.current = { status: "idle" };
|
|
1338
|
+
setIsServerStreaming(false);
|
|
1339
|
+
setIsRecovering(false);
|
|
1340
|
+
protectedStreamingAssistantRef.current = null;
|
|
1341
|
+
localResponseIds.clear();
|
|
1342
|
+
};
|
|
1343
|
+
}, [
|
|
1344
|
+
agent,
|
|
1345
|
+
setMessages,
|
|
1346
|
+
resume,
|
|
1347
|
+
customTransport,
|
|
1348
|
+
preserveProtectedStreamingAssistant,
|
|
1349
|
+
resetToolContinuation,
|
|
1350
|
+
resetMatchingHydratedAssistantForReplay,
|
|
1351
|
+
restoreProtectedStreamingAssistant,
|
|
1352
|
+
resetLocalChatState
|
|
1353
|
+
]);
|
|
1354
|
+
const addToolResultAndSendMessage = async (args) => {
|
|
1355
|
+
const { toolCallId } = args;
|
|
1356
|
+
const toolName = "tool" in args ? args.tool : "";
|
|
1357
|
+
const output = "output" in args ? args.output : void 0;
|
|
1358
|
+
agentRef.current.send(JSON.stringify({
|
|
1359
|
+
type: "cf_agent_tool_result",
|
|
1360
|
+
toolCallId,
|
|
1361
|
+
toolName,
|
|
1362
|
+
output,
|
|
1363
|
+
autoContinue: autoContinueAfterToolResult,
|
|
1364
|
+
clientTools: toolsRef.current ? extractClientToolSchemas(toolsRef.current) : void 0
|
|
1365
|
+
}));
|
|
1366
|
+
setClientToolResults((prev) => new Map(prev).set(toolCallId, output));
|
|
1367
|
+
addToolResult(args);
|
|
1368
|
+
if (autoContinueAfterToolResult) startToolContinuation();
|
|
1369
|
+
if (!autoContinueAfterToolResult) {
|
|
1370
|
+
if (!autoSendAfterAllConfirmationsResolved) {
|
|
1371
|
+
sendMessage();
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
const pending = pendingConfirmationsRef.current?.toolCallIds;
|
|
1375
|
+
if (!pending) {
|
|
1376
|
+
sendMessage();
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1379
|
+
const wasLast = pending.size === 1 && pending.has(toolCallId);
|
|
1380
|
+
if (pending.has(toolCallId)) pending.delete(toolCallId);
|
|
1381
|
+
if (wasLast || pending.size === 0) sendMessage();
|
|
1382
|
+
}
|
|
1383
|
+
};
|
|
1384
|
+
const addToolApprovalResponseAndNotifyServer = (args) => {
|
|
1385
|
+
const { id: approvalId, approved } = args;
|
|
1386
|
+
let toolCallId;
|
|
1387
|
+
for (const msg of messagesRef.current) {
|
|
1388
|
+
for (const part of msg.parts) if ("toolCallId" in part && "approval" in part && part.approval?.id === approvalId) {
|
|
1389
|
+
toolCallId = part.toolCallId;
|
|
1390
|
+
break;
|
|
1391
|
+
}
|
|
1392
|
+
if (toolCallId) break;
|
|
1393
|
+
}
|
|
1394
|
+
if (toolCallId) sendToolApprovalToServer(toolCallId, approved);
|
|
1395
|
+
else console.warn(`[useAgentChat] addToolApprovalResponse: Could not find toolCallId for approval ID "${approvalId}". Server will not be notified, which may cause duplicate messages.`);
|
|
1396
|
+
addToolApprovalResponse(args);
|
|
1397
|
+
};
|
|
1398
|
+
const messagesWithToolResults = useMemo(() => {
|
|
1399
|
+
if (clientToolResults.size === 0) return chatMessages;
|
|
1400
|
+
return chatMessages.map((msg) => ({
|
|
1401
|
+
...msg,
|
|
1402
|
+
parts: msg.parts.map((p) => {
|
|
1403
|
+
if (!("toolCallId" in p) || !("state" in p) || p.state !== "input-available" || !clientToolResults.has(p.toolCallId)) return p;
|
|
1404
|
+
return {
|
|
1405
|
+
...p,
|
|
1406
|
+
state: "output-available",
|
|
1407
|
+
output: clientToolResults.get(p.toolCallId)
|
|
1408
|
+
};
|
|
1409
|
+
})
|
|
1410
|
+
}));
|
|
1411
|
+
}, [chatMessages, clientToolResults]);
|
|
1412
|
+
useEffect(() => {
|
|
1413
|
+
const currentToolCallIds = /* @__PURE__ */ new Set();
|
|
1414
|
+
for (const msg of chatMessages) for (const part of msg.parts) if ("toolCallId" in part && part.toolCallId) currentToolCallIds.add(part.toolCallId);
|
|
1415
|
+
setClientToolResults((prev) => {
|
|
1416
|
+
if (prev.size === 0) return prev;
|
|
1417
|
+
let hasStaleEntries = false;
|
|
1418
|
+
for (const toolCallId of prev.keys()) if (!currentToolCallIds.has(toolCallId)) {
|
|
1419
|
+
hasStaleEntries = true;
|
|
1420
|
+
break;
|
|
1421
|
+
}
|
|
1422
|
+
if (!hasStaleEntries) return prev;
|
|
1423
|
+
const newMap = /* @__PURE__ */ new Map();
|
|
1424
|
+
for (const [id, output] of prev) if (currentToolCallIds.has(id)) newMap.set(id, output);
|
|
1425
|
+
return newMap;
|
|
1426
|
+
});
|
|
1427
|
+
for (const toolCallId of processedToolCalls.current) if (!currentToolCallIds.has(toolCallId)) processedToolCalls.current.delete(toolCallId);
|
|
1428
|
+
}, [chatMessages]);
|
|
1429
|
+
const addToolOutput = useCallback((opts) => {
|
|
1430
|
+
const toolName = opts.toolName ?? "";
|
|
1431
|
+
sendToolOutputToServer(opts.toolCallId, toolName, opts.output, opts.state, opts.errorText);
|
|
1432
|
+
addToolResult({
|
|
1433
|
+
tool: toolName,
|
|
1434
|
+
toolCallId: opts.toolCallId,
|
|
1435
|
+
output: opts.state === "output-error" ? opts.errorText ?? "Tool execution denied by user" : opts.output
|
|
1436
|
+
});
|
|
1437
|
+
}, [sendToolOutputToServer, addToolResult]);
|
|
1438
|
+
const lastAssistantMessage = messagesWithToolResults[messagesWithToolResults.length - 1];
|
|
1439
|
+
const hasPendingClientToolCalls = (() => {
|
|
1440
|
+
if (pendingOnToolCallIds.size === 0 && !tools) return false;
|
|
1441
|
+
if (!lastAssistantMessage || lastAssistantMessage.role !== "assistant") return false;
|
|
1442
|
+
for (const part of lastAssistantMessage.parts) {
|
|
1443
|
+
if (!isToolUIPart(part)) continue;
|
|
1444
|
+
if (part.state !== "input-available") continue;
|
|
1445
|
+
const toolName = getToolName(part);
|
|
1446
|
+
if (toolsRequiringConfirmation.includes(toolName)) continue;
|
|
1447
|
+
if (pendingOnToolCallIds.has(part.toolCallId)) return true;
|
|
1448
|
+
if (tools?.[toolName]?.execute) return true;
|
|
1449
|
+
}
|
|
1450
|
+
return false;
|
|
1451
|
+
})();
|
|
1452
|
+
const effectiveIsServerStreaming = isServerStreaming || hasPendingClientToolCalls;
|
|
1453
|
+
const isStreaming = status === "streaming" || effectiveIsServerStreaming;
|
|
1454
|
+
return {
|
|
1455
|
+
...useChatHelpers,
|
|
1456
|
+
messages: messagesWithToolResults,
|
|
1457
|
+
isServerStreaming: effectiveIsServerStreaming,
|
|
1458
|
+
isStreaming,
|
|
1459
|
+
/**
|
|
1460
|
+
* True while a durable chat turn is being recovered (interrupted by a
|
|
1461
|
+
* deploy/eviction or a stream-stall watchdog abort and now resuming, #1620).
|
|
1462
|
+
* Distinct from `isStreaming` — a recovering turn isn't producing tokens
|
|
1463
|
+
* yet. Render a "recovering…" hint; most UIs treat `isStreaming ||
|
|
1464
|
+
* isRecovering` as "busy". Cleared automatically on the next stream/terminal.
|
|
1465
|
+
*/
|
|
1466
|
+
isRecovering,
|
|
1467
|
+
isToolContinuation,
|
|
1468
|
+
connectionError: agent.connectionError ?? null,
|
|
1469
|
+
sendMessage: sendMessageWithStreamingProtection,
|
|
1470
|
+
stop: stopWithToolContinuationAbort,
|
|
1471
|
+
/**
|
|
1472
|
+
* Provide output for a tool call. Use this for tools that require user interaction
|
|
1473
|
+
* or client-side execution.
|
|
1474
|
+
*/
|
|
1475
|
+
addToolOutput,
|
|
1476
|
+
/**
|
|
1477
|
+
* @deprecated Use `addToolOutput` instead.
|
|
1478
|
+
*/
|
|
1479
|
+
addToolResult: addToolResultAndSendMessage,
|
|
1480
|
+
/**
|
|
1481
|
+
* Respond to a tool approval request. Use this for tools with `needsApproval`.
|
|
1482
|
+
* This wrapper notifies the server before updating local state, preventing
|
|
1483
|
+
* duplicate messages when sendMessage() is called afterward.
|
|
1484
|
+
*/
|
|
1485
|
+
addToolApprovalResponse: addToolApprovalResponseAndNotifyServer,
|
|
1486
|
+
clearHistory: () => {
|
|
1487
|
+
resetLocalChatState();
|
|
1488
|
+
agent.send(JSON.stringify({ type: "cf_agent_chat_clear" }));
|
|
1489
|
+
},
|
|
1490
|
+
setMessages: (messagesOrUpdater) => {
|
|
1491
|
+
let resolvedMessages;
|
|
1492
|
+
if (typeof messagesOrUpdater === "function") resolvedMessages = messagesOrUpdater(messagesRef.current);
|
|
1493
|
+
else resolvedMessages = messagesOrUpdater;
|
|
1494
|
+
if (resolvedMessages.length === 0) markInitialMessagesSeeded();
|
|
1495
|
+
setMessages(resolvedMessages);
|
|
1496
|
+
if (syncMessagesToServer) agent.send(JSON.stringify({
|
|
1497
|
+
messages: resolvedMessages,
|
|
1498
|
+
type: "cf_agent_chat_messages"
|
|
1499
|
+
}));
|
|
1500
|
+
}
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
//#endregion
|
|
1504
|
+
export { WebSocketChatTransport, detectToolsRequiringConfirmation, extractClientToolSchemas, getAgentMessages, getToolApproval, getToolCallId, getToolInput, getToolOutput, getToolPartState, useAgentChat };
|
|
1505
|
+
|
|
1506
|
+
//# sourceMappingURL=react.js.map
|