UNPKG

agents

Version:

A home for your AI agents

1,673 lines 66.3 kB
import { i as transition, r as STREAM_RESUME_NONE_REASONS } from "../wire-types-CU9rLoeS.js";
import { getToolName, isToolUIPart } from "ai";
import { nanoid } from "nanoid";
import { use, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useChat } from "@ai-sdk/react";
//#region src/chat/ws-chat-transport.ts
/**
* Short safety-net timeout for a resume probe when the server has said nothing.
* Under normal operation the server answers a `STREAM_RESUME_REQUEST` with
* `STREAM_RESUMING`, `STREAM_RESUME_NONE`, or `STREAM_PENDING` well before this.
*/
const RESUME_PROBE_TIMEOUT_MS = 5e3;
/**
* Extended backstop applied once the server says a turn is pending
* (`STREAM_PENDING`, #1784). The pre-stream window (queueing, MCP setup,
* debounce, model latency) can exceed the short probe timeout, and the server
* guarantees a follow-up `STREAM_RESUMING` or `STREAM_RESUME_NONE` — so we wait
* much longer (refreshed on every keep-waiting frame) but still cap it so a
* dropped follow-up degrades to a null resolve instead of hanging forever.
*/
const RESUME_PENDING_TIMEOUT_MS = 6e4;
/**
* ChatTransport that sends messages over WebSocket and returns a
* ReadableStream<UIMessageChunk> that the AI SDK's useChat consumes directly.
* No fake fetch, no Response reconstruction, no double SSE parsing.
*/
var WebSocketChatTransport = class {
	constructor(options) {
		this._resumeResolver = null;
		this._resumeNoneResolver = null;
		this._onStreamPending = null;
		this._retryResumeProbe = null;
		this._expectToolContinuation = false;
		this._abortToolContinuation = null;
		this._activeServerTurnId = null;
		this._cancelAttachedStream = null;
		this._detachResumeStream = null;
		this.agent = options.agent;
		this.prepareBody = options.prepareBody;
		this.activeRequestIds = options.activeRequestIds;
		this.cancelOnClientAbort = options.cancelOnClientAbort ?? false;
	}
	/**
	* Point the singleton transport at a new Agent connection. A pending resolver
	* belongs to the old Chat/socket generation and must settle before messages
	* from the replacement connection can be consumed (#1914 review).
	*/
	setAgent(agent) {
		if (this.agent === agent) return;
		this.resetResumeState();
		this.agent = agent;
	}
	setCancelOnClientAbort(cancelOnClientAbort) {
		this.cancelOnClientAbort = cancelOnClientAbort;
	}
	/**
	* Explicitly cancel the active server turn, if any.
	* This is separate from generic client-side abort/cancel lifecycle so
	* clients can detach locally without stopping server work.
	*/
	cancelActiveServerTurn() {
		const requestId = this._activeServerTurnId;
		let cancelledRequest = false;
		if (requestId) {
			this.sendCancelFrame(requestId);
			this._cancelAttachedStream?.();
			this.clearActiveServerTurn(requestId);
			cancelledRequest = true;
		}
		const cancelledToolContinuation = this.abortActiveToolContinuation();
		return cancelledRequest || cancelledToolContinuation;
	}
	sendCancelFrame(requestId) {
		try {
			this.agent.send(JSON.stringify({
				id: requestId,
				type: "cf_agent_chat_request_cancel"
			}));
		} catch {}
	}
	setActiveServerTurn(requestId, cancelAttachedStream) {
		this._activeServerTurnId = requestId;
		this._cancelAttachedStream = cancelAttachedStream;
	}
	clearActiveServerTurn(requestId) {
		if (this._activeServerTurnId === requestId) {
			this._activeServerTurnId = null;
			this._cancelAttachedStream = null;
		}
	}
	/**
	* Mark that the next reconnectToStream() call should attach to a
	* server-initiated tool continuation rather than a page-load resume.
	*/
	expectToolContinuation() {
		this._expectToolContinuation = true;
	}
	/**
	* Abort the active client-side tool continuation stream, if one is attached
	* to a server request id.
	*/
	abortActiveToolContinuation() {
		return this._abortToolContinuation?.() ?? false;
	}
	/**
	* True when the transport is waiting for a resume handshake.
	*/
	isAwaitingResume() {
		return this._resumeResolver !== null || this._resumeNoneResolver !== null;
	}
	/**
	* Settle and detach the current handshake without interpreting it as a
	* server-idle response. Used when the owning hook/agent generation changes.
	*/
	cancelPendingResume() {
		const resolveNone = this._resumeNoneResolver;
		if (!resolveNone) return false;
		return resolveNone({});
	}
	/**
	* Invalidate all client-side resume state for an obsolete hook/agent
	* generation without cancelling its durable server turn.
	*/
	resetResumeState() {
		this._expectToolContinuation = false;
		this.cancelPendingResume();
		this._detachResumeStream?.();
	}
	/**
	* Re-send the active handshake request on the latest socket generation. This
	* preserves one AI SDK resume operation while recovering a request/reply lost
	* with the previous WebSocket.
	*/
	retryPendingResume() {
		const retry = this._retryResumeProbe;
		if (!retry) return false;
		retry();
		return true;
	}
	/**
	* Called by onAgentMessage when it receives CF_AGENT_STREAM_RESUMING.
	* If reconnectToStream is waiting, this handles the resume handshake
	* (ACK + stream creation) and returns true. Otherwise returns false
	* so the caller can use its own fallback path.
	*/
	handleStreamResuming(data) {
		if (!this._resumeResolver) return false;
		this._resumeResolver(data);
		return true;
	}
	/**
	* Called by onAgentMessage when it receives CF_AGENT_STREAM_RESUME_NONE.
	* If reconnectToStream is waiting, resolves the promise with null
	* immediately (no 5-second timeout). Returns true if handled.
	*/
	handleStreamResumeNone(data = {}) {
		if (!this._resumeNoneResolver) return false;
		return this._resumeNoneResolver(data);
	}
	/**
	* Called by onAgentMessage when it receives CF_AGENT_STREAM_PENDING (#1784):
	* the server accepted a turn but its stream has not started yet. If a resume
	* path is awaiting, extend its probe timeout (so it keeps waiting for the
	* eventual STREAM_RESUMING / STREAM_RESUME_NONE instead of resolving null
	* after the short window). Returns true if a waiting path consumed it.
	*/
	handleStreamPending() {
		if (!this._onStreamPending) return false;
		this._onStreamPending();
		return true;
	}
	/**
	* Called by the hook's shared message handler when a server turn finishes
	* outside the currently attached transport stream, such as after local-only
	* client cleanup.
	*/
	handleServerTurnCompleted(requestId) {
		this.clearActiveServerTurn(requestId);
	}
	/**
	* Register a server turn that is being rendered outside a transport-owned
	* stream, such as the hook's fallback cross-tab/resume observer path.
	*/
	observeServerTurn(requestId) {
		this.setActiveServerTurn(requestId, null);
	}
	async sendMessages(options) {
		const requestId = nanoid(8);
		const abortController = new AbortController();
		let completed = false;
		let requestSent = false;
		let extraBody = {};
		if (this.prepareBody) extraBody = await this.prepareBody({
			messages: options.messages,
			trigger: options.trigger,
			messageId: options.messageId
		});
		if (options.body) extraBody = {
			...extraBody,
			...options.body
		};
		const bodyPayload = JSON.stringify({
			messages: options.messages,
			trigger: options.trigger,
			...extraBody
		});
		this.activeRequestIds?.add(requestId);
		const agent = this.agent;
		const activeIds = this.activeRequestIds;
		const finish = (action, keepId = false, clearServerTurn = true) => {
			if (completed) return;
			completed = true;
			if (clearServerTurn) this.clearActiveServerTurn(requestId);
			try {
				action();
			} catch {}
			if (!keepId) activeIds?.delete(requestId);
			abortController.abort();
		};
		const abortError = /* @__PURE__ */ new Error("Aborted");
		abortError.name = "AbortError";
		const cancelActiveRequest = () => {
			if (completed) return false;
			finish(() => streamController.error(abortError), true);
			return true;
		};
		this.setActiveServerTurn(requestId, cancelActiveRequest);
		const onAbort = () => {
			if (completed) return;
			if (this.cancelOnClientAbort) {
				if (requestSent) this.sendCancelFrame(requestId);
				finish(() => streamController.error(abortError), requestSent);
			} else finish(() => streamController.error(abortError), false, !requestSent);
		};
		let streamController;
		const stream = new ReadableStream({
			start(controller) {
				streamController = controller;
				const onMessage = (event) => {
					try {
						const data = JSON.parse(event.data);
						if (data.type !== "cf_agent_use_chat_response") return;
						if (data.id !== requestId) return;
						if (data.error) {
							finish(() => controller.error(new Error(data.body || "Stream error")));
							return;
						}
						if (data.body?.trim()) try {
							const chunk = JSON.parse(data.body);
							controller.enqueue(chunk);
						} catch {}
						if (data.done) finish(() => controller.close());
					} catch {}
				};
				const onClose = () => {
					finish(() => controller.close(), false, false);
				};
				agent.addEventListener("message", onMessage, { signal: abortController.signal });
				agent.addEventListener("close", onClose, { signal: abortController.signal });
			},
			cancel() {
				onAbort();
			}
		});
		if (options.abortSignal) {
			options.abortSignal.addEventListener("abort", onAbort, { once: true });
			if (options.abortSignal.aborted) onAbort();
		}
		if (completed) return stream;
		requestSent = true;
		agent.send(JSON.stringify({
			id: requestId,
			init: {
				method: "POST",
				body: bodyPayload
			},
			type: "cf_agent_use_chat_request"
		}));
		return stream;
	}
	async reconnectToStream(_options) {
		if (this.isAwaitingResume()) return null;
		if (this._expectToolContinuation) {
			this._expectToolContinuation = false;
			return this._createToolContinuationStream();
		}
		const activeIds = this.activeRequestIds;
		const probeId = nanoid(8);
		return new Promise((resolve) => {
			let resolved = false;
			let timeout;
			let resumeResolver = null;
			let resumeNoneResolver = null;
			let onStreamPending = null;
			let retryResumeProbe = null;
			const clearOwnedCallbacks = () => {
				if (resumeResolver && this._resumeResolver === resumeResolver) this._resumeResolver = null;
				if (resumeNoneResolver && this._resumeNoneResolver === resumeNoneResolver) this._resumeNoneResolver = null;
				if (onStreamPending && this._onStreamPending === onStreamPending) this._onStreamPending = null;
				if (retryResumeProbe && this._retryResumeProbe === retryResumeProbe) this._retryResumeProbe = null;
			};
			const done = (value) => {
				if (resolved) return;
				resolved = true;
				clearOwnedCallbacks();
				if (timeout) clearTimeout(timeout);
				resolve(value);
			};
			const armTimeout = (delay) => {
				if (timeout) clearTimeout(timeout);
				timeout = setTimeout(() => done(null), delay);
			};
			onStreamPending = () => {
				if (resolved) return;
				armTimeout(RESUME_PENDING_TIMEOUT_MS);
			};
			resumeNoneResolver = (data) => {
				if (data.probeId && data.probeId !== probeId) return false;
				done(null);
				return true;
			};
			resumeResolver = (data) => {
				const requestId = data.id;
				activeIds?.add(requestId);
				const stream = this._createResumeStream(requestId);
				this.agent.send(JSON.stringify({
					type: "cf_agent_stream_resume_ack",
					id: requestId
				}));
				done(stream);
			};
			retryResumeProbe = () => {
				if (resolved) return;
				armTimeout(RESUME_PROBE_TIMEOUT_MS);
				try {
					this.agent.send(JSON.stringify({
						type: "cf_agent_stream_resume_request",
						probeId
					}));
				} catch {}
			};
			this._onStreamPending = onStreamPending;
			this._resumeNoneResolver = resumeNoneResolver;
			this._resumeResolver = resumeResolver;
			this._retryResumeProbe = retryResumeProbe;
			retryResumeProbe();
		});
	}
	/**
	* Creates a deferred ReadableStream for client-side tool continuations.
	* The stream is returned immediately so AI SDK status becomes "submitted"
	* right after addToolOutput()/addToolApprovalResponse(), then it waits for
	* the server to announce the continuation via STREAM_RESUMING.
	*/
	_createToolContinuationStream() {
		const agent = this.agent;
		const activeIds = this.activeRequestIds;
		const streamController = new AbortController();
		const abortError = /* @__PURE__ */ new Error("Aborted");
		abortError.name = "AbortError";
		let completed = false;
		let requestId = null;
		let readerController = null;
		const probeId = nanoid(8);
		let onResumeRef = null;
		let onResumeNoneRef = null;
		let onStreamPendingRef = null;
		let retryResumeProbeRef = null;
		let abortToolContinuationRef = null;
		let detachResumeStreamRef = null;
		const clearOwnedHandshake = () => {
			if (onResumeRef && this._resumeResolver === onResumeRef) this._resumeResolver = null;
			if (onResumeNoneRef && this._resumeNoneResolver === onResumeNoneRef) this._resumeNoneResolver = null;
			if (onStreamPendingRef && this._onStreamPending === onStreamPendingRef) this._onStreamPending = null;
			if (retryResumeProbeRef && this._retryResumeProbe === retryResumeProbeRef) this._retryResumeProbe = null;
		};
		const finish = (action, keepRequestId = false) => {
			if (completed) return;
			completed = true;
			if (this._abortToolContinuation === abortToolContinuationRef) this._abortToolContinuation = null;
			if (this._detachResumeStream === detachResumeStreamRef) this._detachResumeStream = null;
			clearOwnedHandshake();
			try {
				action();
			} catch {}
			if (requestId && !keepRequestId) activeIds?.delete(requestId);
			streamController.abort();
		};
		const transport = this;
		abortToolContinuationRef = () => {
			if (completed) return false;
			if (requestId === null) {
				finish(() => readerController?.error(abortError));
				return true;
			}
			try {
				agent.send(JSON.stringify({
					type: "cf_agent_chat_request_cancel",
					id: requestId
				}));
			} catch {}
			finish(() => readerController?.error(abortError), true);
			return true;
		};
		this._abortToolContinuation = abortToolContinuationRef;
		detachResumeStreamRef = () => {
			if (completed) return false;
			finish(() => readerController?.close());
			return true;
		};
		this._detachResumeStream = detachResumeStreamRef;
		return new ReadableStream({
			start(controller) {
				readerController = controller;
				let timeout;
				const armTimeout = (delay) => {
					if (timeout) clearTimeout(timeout);
					timeout = setTimeout(() => finish(() => controller.close()), delay);
				};
				const onResumeNone = (data) => {
					if (data.probeId && data.probeId !== probeId) return false;
					finish(() => controller.close());
					return true;
				};
				const onResume = (data) => {
					if (requestId) return;
					requestId = data.id;
					activeIds?.add(requestId);
					clearOwnedHandshake();
					if (timeout) clearTimeout(timeout);
					agent.send(JSON.stringify({
						type: "cf_agent_stream_resume_ack",
						id: requestId
					}));
				};
				const onStreamPending = () => {
					if (completed) return;
					armTimeout(RESUME_PENDING_TIMEOUT_MS);
				};
				const retryResumeProbe = () => {
					if (completed || requestId !== null) return;
					armTimeout(RESUME_PROBE_TIMEOUT_MS);
					try {
						transport.agent.send(JSON.stringify({
							type: "cf_agent_stream_resume_request",
							probeId
						}));
					} catch {}
				};
				onResumeRef = onResume;
				onResumeNoneRef = onResumeNone;
				onStreamPendingRef = onStreamPending;
				retryResumeProbeRef = retryResumeProbe;
				transport._resumeResolver = onResume;
				transport._resumeNoneResolver = onResumeNone;
				transport._onStreamPending = onStreamPending;
				transport._retryResumeProbe = retryResumeProbe;
				const onMessage = (event) => {
					try {
						const data = JSON.parse(event.data);
						if (data.type !== "cf_agent_use_chat_response" || requestId == null || data.id !== requestId) return;
						if (data.error) {
							finish(() => controller.error(new Error(data.body || "Stream error")));
							return;
						}
						if (data.body?.trim()) try {
							const chunk = JSON.parse(data.body);
							controller.enqueue(chunk);
						} catch {}
						if (data.done) finish(() => controller.close());
					} catch {}
				};
				const onClose = () => finish(() => controller.close());
				agent.addEventListener("message", onMessage, { signal: streamController.signal });
				agent.addEventListener("close", onClose, { signal: streamController.signal });
				retryResumeProbe();
			},
			cancel() {
				if (requestId && transport.cancelOnClientAbort) {
					transport.sendCancelFrame(requestId);
					finish(() => {}, true);
				} else finish(() => {});
			}
		});
	}
	/**
	* Creates a ReadableStream that receives resumed stream chunks
	* and forwards them to useChat as UIMessageChunk objects.
	*/
	_createResumeStream(requestId) {
		const agent = this.agent;
		const activeIds = this.activeRequestIds;
		const chunkController = new AbortController();
		const abortError = /* @__PURE__ */ new Error("Aborted");
		abortError.name = "AbortError";
		let completed = false;
		let detachResumeStream = null;
		const finish = (action, keepId = false, clearServerTurn = true) => {
			if (completed) return;
			completed = true;
			if (clearServerTurn) this.clearActiveServerTurn(requestId);
			if (this._detachResumeStream === detachResumeStream) this._detachResumeStream = null;
			try {
				action();
			} catch {}
			if (!keepId) activeIds?.delete(requestId);
			chunkController.abort();
		};
		let streamController = null;
		const cancelActiveRequest = () => {
			if (completed) return false;
			finish(() => streamController?.error(abortError), true);
			return true;
		};
		this.setActiveServerTurn(requestId, cancelActiveRequest);
		const transport = this;
		detachResumeStream = () => {
			if (completed) return false;
			finish(() => streamController?.close());
			return true;
		};
		this._detachResumeStream = detachResumeStream;
		return new ReadableStream({
			start(controller) {
				streamController = controller;
				const onMessage = (event) => {
					try {
						const data = JSON.parse(event.data);
						if (data.type !== "cf_agent_use_chat_response") return;
						if (data.id !== requestId) return;
						if (data.error) {
							finish(() => controller.error(new Error(data.body || "Stream error")));
							return;
						}
						if (data.body?.trim()) try {
							const chunk = JSON.parse(data.body);
							controller.enqueue(chunk);
						} catch {}
						if (data.done) finish(() => controller.close());
					} catch {}
				};
				const onClose = () => {
					finish(() => controller.close(), false, false);
				};
				agent.addEventListener("message", onMessage, { signal: chunkController.signal });
				agent.addEventListener("close", onClose, { signal: chunkController.signal });
			},
			cancel() {
				if (transport.cancelOnClientAbort) {
					transport.sendCancelFrame(requestId);
					finish(() => {}, true);
				} else finish(() => {}, false, false);
			}
		});
	}
};
//#endregion
//#region src/chat/react.tsx
/**
* One-shot deprecation warnings (warns once per key per session).
*/
const _deprecationWarnings = /* @__PURE__ */ new Set();
function warnDeprecated(id, message) {
	if (!_deprecationWarnings.has(id)) {
		_deprecationWarnings.add(id);
		console.warn(`[agents/chat] Deprecated: ${message}`);
	}
}
/**
* Extracts tool schemas from tools that have client-side execute functions.
* These schemas are automatically sent to the server with each request.
*
* Called internally by `useAgentChat` when `tools` are provided.
* Most apps do not need to call this directly.
*
* @param tools - Record of tool name to tool definition
* @returns Array of tool schemas to send to server, or undefined if none
*/
function extractClientToolSchemas(tools) {
	if (!tools) return void 0;
	const schemas = Object.entries(tools).filter(([_, tool]) => tool.execute).map(([name, tool]) => {
		if (tool.inputSchema && !tool.parameters) console.warn(`[useAgentChat] Tool "${name}" uses deprecated 'inputSchema'. Please migrate to 'parameters'.`);
		return {
			name,
			description: tool.description,
			parameters: tool.parameters ?? tool.inputSchema
		};
	});
	return schemas.length > 0 ? schemas : void 0;
}
/**
* Map internal tool part states to simplified UI-relevant states.
*
* @example
* ```tsx
* import { isToolUIPart } from "ai";
* import { getToolPartState } from "@cloudflare/ai-chat/react";
*
* if (isToolUIPart(part)) {
*   const state = getToolPartState(part);
*   if (state === "complete") { ... }
*   if (state === "waiting-approval") { ... }
* }
* ```
*/
function getToolPartState(part) {
	switch (part.state) {
		case "input-streaming": return "streaming";
		case "approval-requested": return "waiting-approval";
		case "approval-responded": return "approved";
		case "output-available": return "complete";
		case "output-error": return "error";
		case "output-denied": return "denied";
		default: return "loading";
	}
}
/** Get the tool call ID from a tool UI part. */
function getToolCallId(part) {
	return part.toolCallId;
}
/** Get the tool input from a tool UI part (if available). */
function getToolInput(part) {
	return part.input;
}
/** Get the tool output from a tool UI part (if available). */
function getToolOutput(part) {
	return part.output;
}
/** Get the approval info from a tool UI part (if in approval state). */
function getToolApproval(part) {
	return part.approval;
}
function agentNameToKebab(name) {
	if (name === name.toUpperCase() && name !== name.toLowerCase()) return name.toLowerCase().replace(/_/g, "-");
	let result = name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
	result = result.startsWith("-") ? result.slice(1) : result;
	return result.replace(/_/g, "-").replace(/-$/, "");
}
/**
* Fetch messages from an agent's `/get-messages` HTTP endpoint.
*
* Use in framework route loaders to prefetch messages before the component
* tree mounts, or anywhere you need messages outside a React hook.
*
* @example Standard routing
* ```typescript
* import { getAgentMessages } from "@cloudflare/ai-chat/react";
*
* const messages = await getAgentMessages({
*   host: "https://my-app.workers.dev",
*   agent: "ChatAgent",
*   name: "session-123"
* });
* ```
*
* @example With basePath (custom URL)
* ```typescript
* const messages = await getAgentMessages({
*   url: "https://my-app.workers.dev/custom/path/get-messages"
* });
* ```
*/
async function getAgentMessages(options) {
	let messagesUrl;
	if ("url" in options) messagesUrl = options.url;
	else {
		const agentSlug = agentNameToKebab(options.agent);
		messagesUrl = `${options.host.endsWith("/") ? options.host.slice(0, -1) : options.host}/agents/${agentSlug}/${options.name}/get-messages`;
	}
	try {
		const response = await fetch(messagesUrl, {
			credentials: options.credentials,
			headers: options.headers
		});
		if (!response.ok) {
			console.warn(`[getAgentMessages] Failed to fetch: ${response.status} ${response.statusText}`);
			return [];
		}
		const text = await response.text();
		if (!text.trim()) return [];
		return JSON.parse(text);
	} catch (error) {
		console.warn("[getAgentMessages] Fetch error:", error);
		return [];
	}
}
/**
* Module-level cache for initial message fetches. Intentionally shared across
* all useAgentChat instances to deduplicate requests during React Strict Mode
* double-renders and re-renders. Cache keys include the agent URL, agent type,
* and thread name to prevent cross-agent collisions.
*/
const requestCache = /* @__PURE__ */ new Map();
function findLastAssistantMessage(messages) {
	for (let index = messages.length - 1; index >= 0; index--) {
		const message = messages[index];
		if (message.role === "assistant") return {
			index,
			message
		};
	}
	return null;
}
function moveMessageToEnd(messages, messageId) {
	const idx = messages.findIndex((m) => m.id === messageId);
	if (idx < 0 || idx === messages.length - 1) return messages;
	const result = [...messages];
	const [msg] = result.splice(idx, 1);
	if (!msg) return messages;
	result.push(msg);
	return result;
}
function prependMissingHydratedMessages(hydratedMessages, currentMessages) {
	if (currentMessages.length === 0) return hydratedMessages;
	const currentMessageIds = new Set(currentMessages.map((message) => message.id));
	const missingHydratedMessages = hydratedMessages.filter((message) => !currentMessageIds.has(message.id));
	if (missingHydratedMessages.length === 0) return currentMessages;
	return [...missingHydratedMessages, ...currentMessages];
}
/**
* React hook for building AI chat interfaces using an Agent
* @param options Chat options including the agent connection
* @returns Chat interface controls and state with added clearHistory method
*/
/**
* Automatically detects which tools require confirmation based on their configuration.
* Tools require confirmation if they have no execute function AND are not server-executed.
* @param tools - Record of tool name to tool definition
* @returns Array of tool names that require confirmation
*
* @deprecated Use `needsApproval` on server-side tools instead.
*/
function detectToolsRequiringConfirmation(tools) {
	warnDeprecated("detectToolsRequiringConfirmation", "detectToolsRequiringConfirmation() is deprecated. Use needsApproval on server-side tools instead. Will be removed in the next major version.");
	if (!tools) return [];
	return Object.entries(tools).filter(([_name, tool]) => !tool.execute).map(([name]) => name);
}
function useAgentChat(options) {
	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;
	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.");
	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.");
	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.");
	const toolsRequiringConfirmation = useMemo(() => {
		if (manualToolsRequiringConfirmation) return manualToolsRequiringConfirmation;
		if (!tools) return [];
		return Object.entries(tools).filter(([_name, tool]) => !tool.execute).map(([name]) => name);
	}, [manualToolsRequiringConfirmation, tools]);
	const onToolCallRef = useRef(onToolCall);
	onToolCallRef.current = onToolCall;
	const onDataRef = useRef(onData);
	onDataRef.current = onData;
	const rawHttpUrl = agent.getHttpUrl();
	const agentUrl = rawHttpUrl ? new URL(rawHttpUrl) : null;
	if (agentUrl) agentUrl.searchParams.delete("_pk");
	const agentUrlString = agentUrl?.toString() ?? null;
	const agentAddressKey = Array.isArray(agent.path) ? JSON.stringify(agent.path.map((step) => [step.agent, step.name])) : JSON.stringify([[agent.agent ?? "", agent.name ?? ""]]);
	const resolvedInitialMessagesCacheKey = agentUrl ? `${agentUrl.origin}${agentUrl.pathname}|${agentAddressKey}` : null;
	const initialMessagesCacheKey = agentAddressKey;
	const stableChatIdRef = useRef(null);
	const previousAgentRef = useRef(null);
	const previousAgentAddressKeyRef = useRef(null);
	const fallbackChatId = agentAddressKey;
	const agentPathChanged = Array.isArray(agent.path) && previousAgentAddressKeyRef.current !== null && previousAgentAddressKeyRef.current !== agentAddressKey;
	if (stableChatIdRef.current === null) stableChatIdRef.current = resolvedInitialMessagesCacheKey ?? fallbackChatId;
	else if (previousAgentRef.current !== agent || agentPathChanged) stableChatIdRef.current = resolvedInitialMessagesCacheKey ?? fallbackChatId;
	previousAgentRef.current = agent;
	previousAgentAddressKeyRef.current = agentAddressKey;
	const agentRef = useRef(agent);
	agentRef.current = agent;
	async function defaultGetInitialMessagesFetch({ url }) {
		if (!url) return [];
		const getMessagesUrl = new URL(url);
		getMessagesUrl.pathname += "/get-messages";
		const response = await fetch(getMessagesUrl.toString(), {
			credentials: options.credentials,
			headers: options.headers
		});
		if (!response.ok) {
			console.warn(`Failed to fetch initial messages: ${response.status} ${response.statusText}`);
			return [];
		}
		const text = await response.text();
		if (!text.trim()) return [];
		try {
			return JSON.parse(text);
		} catch (error) {
			console.warn("Failed to parse initial messages JSON:", error);
			return [];
		}
	}
	const getInitialMessagesFetch = getInitialMessages || defaultGetInitialMessagesFetch;
	function doGetInitialMessages(getInitialMessagesOptions, cacheKey) {
		if (requestCache.has(cacheKey)) return requestCache.get(cacheKey);
		const promise = getInitialMessagesFetch(getInitialMessagesOptions);
		requestCache.set(cacheKey, promise);
		return promise;
	}
	const initialMessagesPromise = !(getInitialMessages === null ? false : getInitialMessages ? true : !!agentUrlString) ? null : doGetInitialMessages({
		agent: agent.agent,
		name: agent.name,
		url: agentUrlString ?? void 0
	}, initialMessagesCacheKey);
	const initialMessages = initialMessagesPromise ? use(initialMessagesPromise) : optionsInitialMessages ?? [];
	useEffect(() => {
		if (!initialMessagesPromise) return;
		requestCache.set(initialMessagesCacheKey, initialMessagesPromise);
		return () => {
			if (requestCache.get(initialMessagesCacheKey) === initialMessagesPromise) requestCache.delete(initialMessagesCacheKey);
		};
	}, [initialMessagesCacheKey, initialMessagesPromise]);
	const toolsRef = useRef(tools);
	toolsRef.current = tools;
	const prepareSendMessagesRequestRef = useRef(prepareSendMessagesRequest);
	prepareSendMessagesRequestRef.current = prepareSendMessagesRequest;
	const bodyOptionRef = useRef(bodyOption);
	bodyOptionRef.current = bodyOption;
	/**
	* Tracks request IDs initiated by this tab via the transport.
	* Used by onAgentMessage to skip messages already handled by the transport.
	*/
	const localRequestIdsRef = useRef(/* @__PURE__ */ new Set());
	const pendingReplayResumeRequestIdsRef = useRef(/* @__PURE__ */ new Set());
	const replayHydratedAssistantMessageIdsRef = useRef(/* @__PURE__ */ new Set());
	/**
	* Request ids this socket already ACKed via the fallback resume path.
	* The server sends CF_AGENT_STREAM_RESUMING for the same request from
	* both onConnect and its CF_AGENT_STREAM_RESUME_REQUEST handler (#1733).
	* The transport-handled path dedupes the second notify via
	* localRequestIdsRef, but the fallback path used to ACK both — triggering
	* a second full-buffer replay that duplicated streamed parts. Entries are
	* dropped when the turn completes; the whole set resets when the socket
	* closes, since a new connection legitimately needs a fresh ACK+replay.
	*/
	const fallbackAckedResumeRequestIdsRef = useRef(/* @__PURE__ */ new Set());
	const customTransportRef = useRef(null);
	if (customTransportRef.current === null) customTransportRef.current = new WebSocketChatTransport({
		agent: agentRef.current,
		activeRequestIds: localRequestIdsRef.current,
		cancelOnClientAbort,
		prepareBody: async ({ messages: msgs, trigger, messageId }) => {
			let extraBody = {};
			const currentBody = bodyOptionRef.current;
			if (currentBody) extraBody = { ...typeof currentBody === "function" ? await currentBody() : currentBody };
			if (toolsRef.current) {
				const clientToolSchemas = extractClientToolSchemas(toolsRef.current);
				if (clientToolSchemas) extraBody.clientTools = clientToolSchemas;
			}
			if (prepareSendMessagesRequestRef.current) {
				const userResult = await prepareSendMessagesRequestRef.current({
					id: agentRef.current._pk,
					messages: msgs,
					trigger,
					messageId
				});
				if (userResult.body) Object.assign(extraBody, userResult.body);
			}
			return extraBody;
		}
	});
	customTransportRef.current.setAgent(agentRef.current);
	customTransportRef.current.setCancelOnClientAbort(cancelOnClientAbort);
	const customTransport = customTransportRef.current;
	const useChatHelpers = useChat({
		...rest,
		onData,
		messages: initialMessages,
		transport: customTransport,
		id: stableChatIdRef.current,
		resume: false
	});
	const { messages: chatMessages, setMessages, addToolResult, addToolApprovalResponse, sendMessage, resumeStream: rawResumeStream, status, stop } = useChatHelpers;
	const statusRef = useRef(status);
	statusRef.current = status;
	const resumeGenerationRef = useRef(0);
	const resumeOperationRef = useRef(null);
	const reconnectProbePendingRef = useRef(false);
	const reconnectProbeRunnerRef = useRef(null);
	const invalidateResumeGeneration = useCallback(() => {
		resumeGenerationRef.current++;
		resumeOperationRef.current = null;
	}, []);
	const resumeStream = useCallback((...args) => {
		const active = resumeOperationRef.current;
		if (active) return active.promise;
		const operation = {
			generation: resumeGenerationRef.current,
			promise: Promise.resolve()
		};
		resumeOperationRef.current = operation;
		operation.promise = rawResumeStream(...args).finally(() => {
			if (resumeOperationRef.current !== operation || resumeGenerationRef.current !== operation.generation) return;
			resumeOperationRef.current = null;
			reconnectProbeRunnerRef.current?.();
		});
		return operation.promise;
	}, [rawResumeStream]);
	const resumeStreamRef = useRef(resumeStream);
	resumeStreamRef.current = resumeStream;
	const resumingToolContinuationRef = useRef(false);
	const pendingToolContinuationRef = useRef(false);
	const observedToolContinuationRequestIdRef = useRef(null);
	const continuationLaunchTimerRef = useRef(null);
	const continuationGenerationRef = useRef(0);
	const [isToolContinuation, setIsToolContinuation] = useState(false);
	const resetToolContinuation = useCallback(() => {
		continuationGenerationRef.current++;
		pendingToolContinuationRef.current = false;
		resumingToolContinuationRef.current = false;
		observedToolContinuationRequestIdRef.current = null;
		if (continuationLaunchTimerRef.current) {
			clearTimeout(continuationLaunchTimerRef.current);
			continuationLaunchTimerRef.current = null;
		}
		setIsToolContinuation(false);
	}, []);
	const scheduleToolContinuationLaunch = useCallback(() => {
		if (!pendingToolContinuationRef.current || statusRef.current !== "ready" || continuationLaunchTimerRef.current) return;
		const timer = setTimeout(() => {
			(async () => {
				while (resumeOperationRef.current) await resumeOperationRef.current.promise.catch(() => {});
				if (continuationLaunchTimerRef.current === timer) continuationLaunchTimerRef.current = null;
				if (!pendingToolContinuationRef.current || statusRef.current !== "ready") return;
				pendingToolContinuationRef.current = false;
				const myGeneration = continuationGenerationRef.current;
				customTransport.expectToolContinuation();
				await resumeStream().catch((error) => {
					console.error("[useAgentChat] Tool continuation resume failed:", error);
				}).finally(() => {
					if (continuationGenerationRef.current !== myGeneration) return;
					resumingToolContinuationRef.current = false;
					setIsToolContinuation(false);
				});
			})();
		}, 0);
		continuationLaunchTimerRef.current = timer;
	}, [customTransport, resumeStream]);
	const startToolContinuation = useCallback(() => {
		if (!autoContinueAfterToolResult || resumingToolContinuationRef.current) return;
		++continuationGenerationRef.current;
		resumingToolContinuationRef.current = true;
		pendingToolContinuationRef.current = true;
		setIsToolContinuation(true);
		scheduleToolContinuationLaunch();
	}, [autoContinueAfterToolResult, scheduleToolContinuationLaunch]);
	useEffect(() => {
		if (status === "error" && pendingToolContinuationRef.current) {
			resetToolContinuation();
			return;
		}
		scheduleToolContinuationLaunch();
	}, [
		resetToolContinuation,
		scheduleToolContinuationLaunch,
		status
	]);
	const stopWithToolContinuationAbort = useCallback(async () => {
		try {
			customTransport.cancelActiveServerTurn();
			await stop();
		} finally {
			customTransport.abortActiveToolContinuation();
		}
	}, [stop, customTransport]);
	const processedToolCalls = useRef(/* @__PURE__ */ new Set());
	const isResolvingToolsRef = useRef(false);
	const [toolResolutionTrigger, setToolResolutionTrigger] = useState(0);
	const [clientToolResults, setClientToolResults] = useState(/* @__PURE__ */ new Map());
	const messagesRef = useRef(chatMessages);
	messagesRef.current = chatMessages;
	const initialMessagesRef = useRef(initialMessages);
	initialMessagesRef.current = initialMessages;
	const seededInitialMessagesKeyRef = useRef(null);
	const markInitialMessagesSeeded = useCallback(() => {
		seededInitialMessagesKeyRef.current = initialMessagesCacheKey;
	}, [initialMessagesCacheKey]);
	useEffect(() => {
		if (!initialMessagesPromise) return;
		if (seededInitialMessagesKeyRef.current === initialMessagesCacheKey) return;
		markInitialMessagesSeeded();
		setMessages((prevMessages) => prependMissingHydratedMessages(initialMessagesRef.current, prevMessages));
	}, [
		initialMessagesCacheKey,
		initialMessagesPromise,
		markInitialMessagesSeeded,
		setMessages
	]);
	const localResponseMessageIdsRef = useRef(/* @__PURE__ */ new Map());
	const protectedStreamingAssistantRef = useRef(null);
	const preserveProtectedStreamingAssistant = useCallback((messages) => {
		const protection = protectedStreamingAssistantRef.current;
		if (!protection) return [...messages];
		const protectedIndex = messages.findIndex((message) => message.id === protection.assistantId);
		if (protectedIndex >= 0 && messages.slice(protectedIndex + 1).some((message) => message.role === "assistant")) {
			protectedStreamingAssistantRef.current = null;
			return [...messages];
		}
		const protectedAssistant = messagesRef.current.find((message) => message.id === protection.assistantId) ?? messages.find((message) => message.id === protection.assistantId);
		if (!protectedAssistant) return [...messages];
		return [...messages.filter((message) => message.id !== protection.assistantId), protectedAssistant];
	}, []);
	const protectStreamingAssistantTail = useCallback(() => {
		if (statusRef.current !== "streaming") return;
		const assistantInfo = findLastAssistantMessage(messagesRef.current);
		if (!assistantInfo) return;
		if (protectedStreamingAssistantRef.current?.assistantId !== assistantInfo.message.id) protectedStreamingAssistantRef.current = {
			assistantId: assistantInfo.message.id,
			anchorMessageId: messagesRef.current[assistantInfo.index - 1]?.id ?? null
		};
		setMessages((prevMessages) => {
			const protection = protectedStreamingAssistantRef.current;
			if (!protection) return prevMessages;
			return moveMessageToEnd(prevMessages, protection.assistantId);
		});
	}, [setMessages]);
	const restoreProtectedStreamingAssistant = useCallback((assistantId) => {
		const protection = protectedStreamingAssistantRef.current;
		if (!protection || assistantId !== void 0 && protection.assistantId !== assistantId) return;
		protectedStreamingAssistantRef.current = null;
		setMessages((prevMessages) => {
			const sourceIdx = prevMessages.findIndex((m) => m.id === protection.assistantId);
			if (sourceIdx < 0) return prevMessages;
			const result = [...prevMessages];
			const [msg] = result.splice(sourceIdx, 1);
			if (!msg) return prevMessages;
			if (protection.anchorMessageId === null) result.unshift(msg);
			else {
				const anchorIdx = result.findIndex((m) => m.id === protection.anchorMessageId);
				result.splice(anchorIdx >= 0 ? anchorIdx + 1 : sourceIdx, 0, msg);
			}
			return result;
		});
	}, [setMessages]);
	const resetMatchingHydratedAssistantForReplay = useCallback((messageId) => {
		setMessages((prevMessages) => {
			const lastMessage = prevMessages[prevMessages.length - 1];
			if (!lastMessage || lastMessage.role !== "assistant" || lastMessage.id !== messageId) return prevMessages;
			replayHydratedAssistantMessageIdsRef.current.add(messageId);
			const next = [...prevMessages];
			next[next.length - 1] = {
				...lastMessage,
				parts: []
			};
			return next;
		});
	}, [setMessages]);
	const collapseHydratedReplayTextParts = useCallback((message) => {
		const parts = message.parts;
		const nextParts = parts.filter((part, index) => {
			if (part.type !== "text" || !("text" in part) || !part.text) return true;
			return !parts.some((candidate, candidateIndex) => {
				if (candidateIndex <= index) return false;
				if (candidate.type !== "text" || !("text" in candidate) || !candidate.text) return false;
				return candidate.text.startsWith(part.text);
			});
		});
		return nextParts.length === parts.length ? message : {
			...message,
			parts: nextParts
		};
	}, []);
	useEffect(() => {
		if (replayHydratedAssistantMessageIdsRef.current.size === 0) return;
		const idsToCollapse = new Set(chatMessages.filter((message) => replayHydratedAssistantMessageIdsRef.current.has(message.id) && message.role === "assistant" && collapseHydratedReplayTextParts(message) !== message).map((message) => message.id));
		if (idsToCollapse.size === 0) return;
		setMessages((prevMessages) => {
			let changed = false;
			const nextMessages = prevMessages.map((message) => {
				if (!idsToCollapse.has(message.id)) return message;
				const nextMessage = collapseHydratedReplayTextParts(message);
				if (nextMessage !== message) changed = true;
				return nextMessage;
			});
			return changed ? nextMessages : prevMessages;
		});
	}, [
		chatMessages,
		collapseHydratedReplayTextParts,
		setMessages
	]);
	const resetLocalChatState = useCallback(() => {
		markInitialMessagesSeeded();
		setMessages([]);
		setClientToolResults(/* @__PURE__ */ new Map());
		setPendingOnToolCallIds(/* @__PURE__ */ new Set());
		resetToolContinuation();
		processedToolCalls.current.clear();
		localResponseMessageIdsRef.current.clear();
		pendingReplayResumeRequestIdsRef.current.clear();
		fallbackAckedResumeRequestIdsRef.current.clear();
		replayHydratedAssistantMessageIdsRef.current.clear();
		protectedStreamingAssistantRef.current = null;
	}, [
		markInitialMessagesSeeded,
		setMessages,
		resetToolContinuation
	]);
	const sendMessageWithStreamingProtection = useCallback(async (message, options) => {
		const request = sendMessage(message, options);
		if (message !== void 0 && !(typeof message === "object" && message !== null && "messageId" in message && message.messageId != null)) protectStreamingAssistantTail();
		return request;
	}, [sendMessage, protectStreamingAssistantTail]);
	const lastMessage = chatMessages[chatMessages.length - 1];
	const pendingConfirmations = (() => {
		if (!lastMessage || lastMessage.role !== "assistant") return {
			messageId: void 0,
			toolCallIds: /* @__PURE__ */ new Set()
		};
		const pendingIds = /* @__PURE__ */ new Set();
		for (const part of lastMessage.parts ?? []) if (isToolUIPart(part) && part.state === "input-available" && toolsRequiringConfirmation.includes(getToolName(part))) pendingIds.add(part.toolCallId);
		return {
			messageId: lastMessage.id,
			toolCallIds: pendingIds
		};
	})();
	const pendingConfirmationsRef = useRef(pendingConfirmations);
	pendingConfirmationsRef.current = pendingConfirmations;
	const [pendingOnToolCallIds, setPendingOnToolCallIds] = useState(() => /* @__PURE__ */ new Set());
	const finishOnToolCall = useCallback((toolCallId) => {
		setPendingOnToolCallIds((prev) => {
			if (!prev.has(toolCallId)) return prev;
			const next = new Set(prev);
			next.delete(toolCallId);
			return next;
		});
	}, []);
	useEffect(() => {
		if (!experimental_automaticToolResolution) return;
		if (isResolvingToolsRef.current) return;
		const lastMsg = chatMessages[chatMessages.length - 1];
		if (!lastMsg || lastMsg.role !== "assistant") return;
		const toolCalls = lastMsg.parts.filter((part) => isToolUIPart(part) && part.state === "input-available" && !processedToolCalls.current.has(part.toolCallId));
		if (toolCalls.length > 0) {
			const currentTools = toolsRef.current;
			const toolCallsToResolve = toolCalls.filter((part) => isToolUIPart(part) && !toolsRequiringConfirmation.includes(getToolName(part)) && currentTools?.[getToolName(part)]?.execute);
			if (toolCallsToResolve.length > 0) {
				isResolvingToolsRef.current = true;
				(async () => {
					try {
						const toolResults = [];
						for (const part of toolCallsToResolve) if (isToolUIPart(part)) {
							let toolOutput = null;
							const toolName = getToolName(part);
							const tool = currentTools?.[toolName];
							if (tool?.execute && part.input !== void 0) try {
								toolOutput = await tool.execute(part.input);
							} catch (error) {
								toolOutput = `Error executing tool: ${error instanceof Error ? error.message : String(error)}`;
							}
							processedToolCalls.current.add(part.toolCallId);
							toolResults.push({
								toolCallId: part.toolCallId,
								toolName,
								output: toolOutput
							});
						}
						if (toolResults.length > 0) {
							const clientToolSchemas = extractClientToolSchemas(currentTools);
							for (const result of toolResults) agentRef.current.send(JSON.stringify({
								type: "cf_agent_tool_result",
								toolCallId: result.toolCallId,
								toolName: result.toolName,
								output: result.output,
								autoContinue: autoContinueAfterToolResult,
								clientTools: clientToolSchemas
							}));
							await Promise.all(toolResults.map((result) => addToolResult({
								tool: result.toolName,
								toolCallId: result.toolCallId,
								output: result.output
							})));
							setClientToolResults((prev) => {
								const newMap = new Map(prev);
								for (const result of toolResults) newMap.set(result.toolCallId, result.output);
								return newMap;
							});
							startToolContinuation();
						}
					} finally {
						isResolvingToolsRef.current = false;
						setToolResolutionTrigger((c) => c + 1);
					}
				})();
			}
		}
	}, [
		chatMessages,
		experimental_automaticToolResolution,
		addToolResult,
		toolsRequiringConfirmation,
		autoContinueAfterToolResult,
		startToolContinuation,
		toolResolutionTrigger
	]);
	const sendToolOutputToServer = useCallback((toolCallId, toolName, output, state, errorText) => {
		const shouldAutoContinue = state === "output-error" ? false : autoContinueAfterToolResult;
		agentRef.current.send(JSON.stringify({
			type: "cf_agent_tool_result",
			toolCallId,
			toolName,
			output,
			...state ? { state } : {},
			...errorText !== void 0 ? { errorText } : {},
			autoContinue: shouldAutoContinue,
			clientTools: toolsRef.current ? extractClientToolSchemas(toolsRef.current) : void 0
		}));
		if (state !== "output-error") setClientToolResults((prev) => new Map(prev).set(toolCallId, output));
		if (shouldAutoContinue) startToolContinuation();
	}, [autoContinueAfterToolResult, startToolContinuation]);
	const sendToolApprovalToServer = useCallback((toolCallId, approved) => {
		agentRef.current.send(JSON.stringify({
			type: "cf_agent_tool_approval",
			toolCallId,
			approved,
			autoContinue: autoContinueAfterToolResult
		}));
		if (autoContinueAfterToolResult) startToolContinuation();
	}, [autoContinueAfterToolResult, startToolContinuation]);
	useEffect(() => {
		const currentOnToolCall = onToolCallRef.current;
		if (!currentOnToolCall) return;
		const lastMsg = chatMessages[chatMessages.length - 1];
		if (!lastMsg || lastMsg.role !== "assistant") return;
		const pendingToolCalls = lastMsg.parts.filter((part) => isToolUIPart(part) && part.state === "input-available" && !processedToolCalls.current.has(part.toolCallId));
		for (const part of pendingToolCalls) if (isToolUIPart(part)) {
			const toolCallId = part.toolCallId;
			const toolName = getToolName(part);
			processedToolCalls.current.add(toolCallId);
			setPendingOnToolCallIds((prev) => {
				if (prev.has(toolCallId)) return prev;
				const next = new Set(prev);
				next.add(toolCallId);
				return next;
			});
			const addToolOutput = (opts) => {
				sendToolOutputToServer(opts.toolCallId, toolName, opts.output, opts.state, opts.errorText);
				addToolResult({
					tool: toolName,
					toolCallId: opts.toolCallId,
					output: opts.state === "output-error" ? opts.errorText ?? "Tool execution denied by user" : opts.output
				});
			};
			let result;
			try {
				result = currentOnToolCall({
					toolCall: {
						toolCallId,
						toolName,
						input: part.input
					},
					addToolOutput
				});
			} catch (error) {
				finishOnToolCall(toolCallId);
				throw error;
			}
			Promise.resolve(result).finally(() => {
				finishOnToolCall(toolCallId);
			});
		}
	}, [
		chatMessages,
		sendToolOutputToServer,
		addToolResult,
		finishOnToolCall
	]);
	const streamStateRef = useRef({ status: "idle" });
	const [isServerStreaming, setIsServerStreaming] = useState(false);
	const [isRecovering, setIsRecovering] = useState(false);
	useEffect(() => {
		const localResponseIds = localResponseMessageIdsRef.current;
		/**
		* Unified message handler that parses JSON once and dispatches based on type.
		* Avoids duplicate parsing overhead from separate listeners.
		*/
		function onAgentMessage(event) {
			if (typeof event.data !== "string") return;
			let data;
			try {
				data = JSON.parse(event.data);
			} catch (_error) {
				return;
			}
			switch (data.type) {
				case "cf_agent_chat_clear":
					streamStateRef.current = transition(streamStateRef.current, { type: "clear" }).state;
					setIsServerStreaming(false);
					setIsRecovering(false);
					resetLocalChatState();
					break;
				case "cf_agent_chat_recovering":
					setIsRecovering(Boolean(data.recovering));
					break;
				case "cf_agent_chat_messages": {
					let next = preserveProtectedStreamingAssistant(data.messages);
					const observed = streamStateRef.current;
					if (observed.status === "observing" && observed.accumulator.parts.length > 0) {
						const snapshotIdx = next.findIndex((m) => m.id === observed.accumulator.messageId);
						const snapshotParts = snapshotIdx >= 0 ? next[snapshotIdx].parts.length : 0;
						if (observed.accumulator.parts.length >= snapshotParts) next = observed.accumulator.mergeInto(next);
					}
					setMessages(next);
					break;
				}
				case "cf_agent_message_updated":
					setMessages((prevMessages) => {
						const updatedMessage = data.message;
						let idx = prevMessages.findIndex((m) => m.id === updatedMessage.id);
						if (idx < 0) {
							const updatedToolCallIds = new Set(updatedMessage.parts.filter((p) => "toolCallId" in p && p.toolCallId).map((p) => p.toolCallId));
							if (updatedToolCallIds.size > 0) idx = prevMessages.findIndex((m) => m.parts.some((p) => "toolCallId" in p && updatedToolCallIds.has(p.toolCallId)));
						}
						if (idx >= 0) {
							const updated = [...prevMessages];
							updated[idx] = {
								...updatedMessage,
								id: prevMessages[idx].id
							};
							return updated;
						}
						return prevMessages;
					});
					break;
				case "cf_agent_stream_resume_none":
					if (customTransport.handleStreamResumeNone(data) && data.reason === STREAM_RESUME_NONE_REASONS.IDLE && typeof data.probeId === "string") {
						const result = transition(streamStateRef.current, { type: "clear" });
						streamStateRef.current = result.state;
						setIsServerStreaming(result.isStreaming);
						if (observedToolContinuationRequestIdRef.current !== null) resetToolContinuation();
					}
					break;
				case "cf_agent_stream_pending":
					customTransport.handleStreamPending();
					break;
				case "cf_agent_stream_resuming": {
					const isEarlyToolContinuation = resumingToolContinuationRef.current && !customTransport.isAwaitingResume();
					if (!resume && !customTransport.isAwaitingResume()) {
						if (!isEarlyToolContinuation) return;
					}
					if (!resumingToolContinuationRef.current) pendingReplayResumeRequestIdsRef.current.add(data.id);
					if (customTransport.handleStreamResuming(data)) return;
					if (localRequestIdsRef.current.has(data.id)) return;
					if (fallbackAckedResumeRequestIdsRef.current.has(data.id)) return;
					if (isEarlyToolContinuation) {
						pendingToolContinuationRef.current = false;
						observedToolContinuationRequestIdRef.current = data.id;
						if (continuationLaunchTimerRef.current) {
							clearTimeout(continuationLaunchTimerRef.current);
							continuationLaunchTimerRef.current = null;
						}
					}
					streamStateRef.current = transition(streamStateRef.current, {
						type: "resume-fallback",
						streamId: data.id,
						messageId: nanoid()
					}).state;
					customTransport.observeServerTurn(data.id);
					setIsServerStreaming(true);
					setIsRecovering(false);
					fallbackAckedResumeRequestIdsRef.current.add(data.id);
					agentRef.current.send(JSON.stringify({
						type: "cf_agent_stream_resume_ack",
						id: data.id
					}));
					break;
				}
				case "cf_agent_use_chat_response": {
					if (localRequestIdsRef.current.has(data.id)) {
						if (data.body?.trim()) try {
							const chunkData = JSON.parse(data.body);
							if (chunkData.type === "start" && typeof chunkData.messageId === "string") {
								localResponseIds.set(data.id, chunkData.messageId);
								if (!data.continuation) {
									if (protectedStreamingAssistantRef.current?.assistantId !== chunkData.messageId) {
										const msgs = messagesRef.current;
										const idx = msgs.findIndex((m) => m.id === chunkData.messageId);
										const anchorMessageId = idx >= 0 ? msgs[idx - 1]?.id ?? null : msgs[msgs.length - 1]?.id ?? null;
										protectedStreamingAssistantRef.current = {
											assistantId: chunkData.messageId,
											anchorMessageId
										};
									}
								}
								if (data.replay && !data.continuation && !resumingToolContinuationRef.current && observedToolContinuationRequestIdRef.current !== data.id) {
									pendingReplayResumeRequestIdsRef.current.delete(data.id);
									resetMatchingHydratedAssistantForReplay(chunkData.messageId);
								}
							}
						} catch {}
						if (data.done || data.replayComplete) pendingReplayResumeRequestIdsRef.current.delete(data.id);
						if (data.done) {
							if (streamStateRef.current.status === "observing" && streamStateRef.current.streamId === data.id) {
								streamStateRef.current = { status: "idle" };
								setIsServerStreaming(false);
							}
							customTransport.handleServerTurnCompleted(data.id);
							restoreProtectedStreamingAssistant(localResponseIds.get(data.id));
							localResponseIds.delete(data.id);
							localRequestIdsRef.current.delete(data.id);
							fallbackAckedResumeRequestIdsRef.current.delete(data.id);
							if (observedToolContinuationRequestIdRef.current === data.id) resetToolContinuation();
						}
						return;
					}
					let chunkData;
					if (data.replay && streamStateRef.current.status !== "observing" && !pendingReplayResumeRequestIdsRef.current.has(data.id)) return;
					if (data.body?.trim()) try {
						chunkData = JSON.parse(data.body);
						if (data.replay && !data.continuation && !resumingToolContinuationRef.current && observedToolContinuationRequestIdRef.current !== data.id && typeof chunkData.messageId === "string" && chunkData.type === "start") {
							pendingReplayResumeRequestIdsRef.current.delete(data.id);
							resetMatchingHydratedAssistantForReplay(chunkData.messageId);
						}
						if (typeof chunkData.type === "string" && chunkData.type.startsWith("data-") && onDataRef.current) onDataRef.current(chunkData);
					} catch (parseError) {
						console.warn("[useAgentChat] Failed to parse stream chunk:", parseError instanceof Error ? parseError.message : parseError, "body:", data.body?.slice(0, 100));
					}
					if (data.done || data.replayComplete) pendingReplayResumeRequestIdsRef.current.delete(data.id);
					if (data.done) {
						customTransport.handleServerTurnCompleted(data.id);
						fallbackAckedResumeRequestIdsRef.current.delete(data.id);
						setIsRecovering(false);
					}
					const completedObservedToolContinuation = data.done && observedToolContinuationRequestIdRef.current === data.id;
					const result = transition(streamStateRef.current, {
						type: "response",
						streamId: data.id,
						messageId: nanoid(),
						chunkData,
						done: data.done,
						error: data.error,
						replay: data.replay,
						replayComplete: data.replayComplete,
						continuation: data.continuation,
						currentMessages: data.continuation ? messagesRef.current : void 0
					});
					streamStateRef.current = result.state;
					if (result.messagesUpdate) setMessages(result.messagesUpdate);
					setIsServerStreaming(result.isStreaming);
					if (completedObservedToolContinuation) resetToolContinuation();
					break;
				}
			}
		}
		const fallbackAckedResumeRequestIds = fallbackAckedResumeRequestIdsRef.current;
		let socketIsOpen = false;
		let sawClose = false;
		let disposed = false;
		const clearFallbackObserver = () => {
			const result = transition(streamStateRef.current, { type: "clear" });
			streamStateRef.current = result.state;
			setIsServerStreaming(result.isStreaming);
			if (observedToolContinuationRequestIdRef.current !== null) resetToolContinuation();
		};
		const tryPendingReconnectProbe = () => {
			if (disposed || !socketIsOpen || !reconnectProbePendingRef.current) return;
			if (!resume) {
				reconnectProbePendingRef.current = false;
				return;
			}
			if (customTransport.retryPendingResume()) {
				reconnectProbePendingRef.current = false;
				return;
			}
			const canReconcileObservedContinuation = observedToolContinuationRequestIdRef.current !== null;
			if (statusRef.current !== "ready" && statusRef.current !== "error" || resumingToolContinuationRef.current && !canReconcileObservedContinuation || resumeOperationRef.current !== null) return;
			reconnectProbePendingRef.current = false;
			resumeStreamRef.current().catch(() => {});
		};
		reconnectProbeRunnerRef.current = tryPendingReconnectProbe;
		function onAgentClose() {
			socketIsOpen = false;
			sawClose = true;
			fallbackAckedResumeRequestIds.clear();
			if (!resume) clearFallbackObserver();
		}
		function onAgentOpen() {
			socketIsOpen = true;
			if (!sawClose) return;
			sawClose = false;
			reconnectProbePendingRef.current = true;
			tryPendingReconnectProbe();
		}
		agent.addEventListener("message", onAgentMessage);
		agent.addEventListener("close", onAgentClose);
		agent.addEventListener("open", onAgentOpen);
		return () => {
			disposed = true;
			if (reconnectProbeRunnerRef.current === tryPendingReconnectProbe) reconnectProbeRunnerRef.current = null;
			reconnectProbePendingRef.current = false;
			agent.removeEventListener("message", onAgentMessage);
			agent.removeEventListener("close", onAgentClose);
			agent.removeEventListener("open", onAgentOpen);
			fallbackAckedResumeRequestIds.clear();
			streamStateRef.current = { status: "idle" };
			setIsServerStreaming(false);
			setIsRecovering(false);
			protectedStreamingAssistantRef.current = null;
			localResponseIds.clear();
			customTransport.resetResumeState();
			invalidateResumeGeneration();
		};
	}, [
		agent,
		setMessages,
		resume,
		customTransport,
		preserveProtectedStreamingAssistant,
		resetToolContinuation,
		resetMatchingHydratedAssistantForReplay,
		restoreProtectedStreamingAssistant,
		resetLocalChatState,
		invalidateResumeGeneration
	]);
	useEffect(() => {
		if (!resume) return;
		resumeStream().catch(() => {});
	}, [resume, resumeStream]);
	useEffect(() => {
		reconnectProbeRunnerRef.current?.();
	}, [isToolContinuation, status]);
	const addToolResultAndSendMessage = async (args) => {
		const { toolCallId } = args;
		const toolName = "tool" in args ? args.tool : "";
		const output = "output" in args ? args.output : void 0;
		agentRef.current.send(JSON.stringify({
			type: "cf_agent_tool_result",
			toolCallId,
			toolName,
			output,
			autoContinue: autoContinueAfterToolResult,
			clientTools: toolsRef.current ? extractClientToolSchemas(toolsRef.current) : void 0
		}));
		setClientToolResults((prev) => new Map(prev).set(toolCallId, output));
		addToolResult(args);
		if (autoContinueAfterToolResult) startToolContinuation();
		if (!autoContinueAfterToolResult) {
			if (!autoSendAfterAllConfirmationsResolved) {
				sendMessage();
				return;
			}
			const pending = pendingConfirmationsRef.current?.toolCallIds;
			if (!pending) {
				sendMessage();
				return;
			}
			const wasLast = pending.size === 1 && pending.has(toolCallId);
			if (pending.has(toolCallId)) pending.delete(toolCallId);
			if (wasLast || pending.size === 0) sendMessage();
		}
	};
	const addToolApprovalResponseAndNotifyServer = (args) => {
		const { id: approvalId, approved } = args;
		let toolCallId;
		for (const msg of messagesRef.current) {
			for (const part of msg.parts) if ("toolCallId" in part && "approval" in part && part.approval?.id === approvalId) {
				toolCallId = part.toolCallId;
				break;
			}
			if (toolCallId) break;
		}
		if (toolCallId) sendToolApprovalToServer(toolCallId, approved);
		else console.warn(`[useAgentChat] addToolApprovalResponse: Could not find toolCallId for approval ID "${approvalId}". Server will not be notified, which may cause duplicate messages.`);
		addToolApprovalResponse(args);
	};
	const messagesWithToolResults = useMemo(() => {
		if (clientToolResults.size === 0) return chatMessages;
		return chatMessages.map((msg) => ({
			...msg,
			parts: msg.parts.map((p) => {
				if (!("toolCallId" in p) || !("state" in p) || p.state !== "input-available" || !clientToolResults.has(p.toolCallId)) return p;
				return {
					...p,
					state: "output-available",
					output: clientToolResults.get(p.toolCallId)
				};
			})
		}));
	}, [chatMessages, clientToolResults]);
	useEffect(() => {
		const currentToolCallIds = /* @__PURE__ */ new Set();
		for (const msg of chatMessages) for (const part of msg.parts) if ("toolCallId" in part && part.toolCallId) currentToolCallIds.add(part.toolCallId);
		setClientToolResults((prev) => {
			if (prev.size === 0) return prev;
			let hasStaleEntries = false;
			for (const toolCallId of prev.keys()) if (!currentToolCallIds.has(toolCallId)) {
				hasStaleEntries = true;
				break;
			}
			if (!hasStaleEntries) return prev;
			const newMap = /* @__PURE__ */ new Map();
			for (const [id, output] of prev) if (currentToolCallIds.has(id)) newMap.set(id, output);
			return newMap;
		});
		for (const toolCallId of processedToolCalls.current) if (!currentToolCallIds.has(toolCallId)) processedToolCalls.current.delete(toolCallId);
	}, [chatMessages]);
	const addToolOutput = useCallback((opts) => {
		const toolName = opts.toolName ?? "";
		sendToolOutputToServer(opts.toolCallId, toolName, opts.output, opts.state, opts.errorText);
		addToolResult({
			tool: toolName,
			toolCallId: opts.toolCallId,
			output: opts.state === "output-error" ? opts.errorText ?? "Tool execution denied by user" : opts.output
		});
	}, [sendToolOutputToServer, addToolResult]);
	const lastAssistantMessage = messagesWithToolResults[messagesWithToolResults.length - 1];
	const hasPendingClientToolCalls = (() => {
		if (pendingOnToolCallIds.size === 0 && !tools) return false;
		if (!lastAssistantMessage || lastAssistantMessage.role !== "assistant") return false;
		for (const part of lastAssistantMessage.parts) {
			if (!isToolUIPart(part)) continue;
			if (part.state !== "input-available") continue;
			const toolName = getToolName(part);
			if (toolsRequiringConfirmation.includes(toolName)) continue;
			if (pendingOnToolCallIds.has(part.toolCallId)) return true;
			if (tools?.[toolName]?.execute) return true;
		}
		return false;
	})();
	const effectiveIsServerStreaming = isServerStreaming || hasPendingClientToolCalls;
	const isStreaming = status === "streaming" || effectiveIsServerStreaming;
	return {
		...useChatHelpers,
		resumeStream,
		messages: messagesWithToolResults,
		isServerStreaming: effectiveIsServerStreaming,
		isStreaming,
		/**
		* True while a durable chat turn is being recovered (interrupted by a
		* deploy/eviction or a stream-stall watchdog abort and now resuming, #1620).
		* Distinct from `isStreaming` — a recovering turn isn't producing tokens
		* yet. Render a "recovering…" hint; most UIs treat `isStreaming ||
		* isRecovering` as "busy". Cleared automatically on the next stream/terminal.
		*/
		isRecovering,
		isToolContinuation,
		connectionError: agent.connectionError ?? null,
		sendMessage: sendMessageWithStreamingProtection,
		stop: stopWithToolContinuationAbort,
		/**
		* Provide output for a tool call. Use this for tools that require user interaction
		* or client-side execution.
		*/
		addToolOutput,
		/**
		* @deprecated Use `addToolOutput` instead.
		*/
		addToolResult: addToolResultAndSendMessage,
		/**
		* Respond to a tool approval request. Use this for tools with `needsApproval`.
		* This wrapper notifies the server before updating local state, preventing
		* duplicate messages when sendMessage() is called afterward.
		*/
		addToolApprovalResponse: addToolApprovalResponseAndNotifyServer,
		clearHistory: () => {
			resetLocalChatState();
			agent.send(JSON.stringify({ type: "cf_agent_chat_clear" }));
		},
		setMessages: (messagesOrUpdater) => {
			let resolvedMessages;
			if (typeof messagesOrUpdater === "function") resolvedMessages = messagesOrUpdater(messagesRef.current);
			else resolvedMessages = messagesOrUpdater;
			if (resolvedMessages.length === 0) markInitialMessagesSeeded();
			setMessages(resolvedMessages);
			if (syncMessagesToServer) agent.send(JSON.stringify({
				messages: resolvedMessages,
				type: "cf_agent_chat_messages"
			}));
		}
	};
}
//#endregion
export { WebSocketChatTransport, detectToolsRequiringConfirmation, extractClientToolSchemas, getAgentMessages, getToolApproval, getToolCallId, getToolInput, getToolOutput, getToolPartState, useAgentChat };

//# sourceMappingURL=react.js.map