import { t as MessageType } from "./ai-types-DEtF_8Km.js";
import { DefaultChatTransport, getToolName, isToolUIPart } from "ai";
import { nanoid } from "nanoid";
import { useChat } from "@ai-sdk/react";
import { use, useCallback, useEffect, useMemo, useRef, useState } from "react";
//#region src/ai-react.tsx
/**
* Extracts tool schemas from tools that have client-side execute functions.
* These schemas are automatically sent to the server with each request.
* @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$1]) => tool$1.execute).map(([name, tool$1]) => {
if (tool$1.inputSchema && !tool$1.parameters) console.warn(`[useAgentChat] Tool "${name}" uses deprecated 'inputSchema'. Please migrate to 'parameters'.`);
return {
name,
description: tool$1.description,
parameters: tool$1.parameters ?? tool$1.inputSchema
};
});
return schemas.length > 0 ? schemas : void 0;
}
const requestCache = /* @__PURE__ */ new Map();
/**
* 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
*/
function detectToolsRequiringConfirmation(tools) {
if (!tools) return [];
return Object.entries(tools).filter(([_name, tool$1]) => !tool$1.execute).map(([name]) => name);
}
function useAgentChat(options) {
const { agent, getInitialMessages, messages: optionsInitialMessages, experimental_automaticToolResolution, tools, toolsRequiringConfirmation: manualToolsRequiringConfirmation, autoContinueAfterToolResult = false, autoSendAfterAllConfirmationsResolved = true, resume = true, prepareSendMessagesRequest, ...rest } = options;
const toolsRequiringConfirmation = manualToolsRequiringConfirmation ?? detectToolsRequiringConfirmation(tools);
const agentUrl = new URL(`${(agent._url || agent._pkurl)?.replace("ws://", "http://").replace("wss://", "https://")}`);
agentUrl.searchParams.delete("_pk");
const agentUrlString = agentUrl.toString();
const initialMessagesCacheKey = `${agentUrlString}|${agent.agent ?? ""}|${agent.name ?? ""}`;
const agentRef = useRef(agent);
useEffect(() => {
agentRef.current = agent;
}, [agent]);
async function defaultGetInitialMessagesFetch({ url }) {
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 ? null : doGetInitialMessages({
agent: agent.agent,
name: agent.name,
url: agentUrlString
}, 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 aiFetch = useCallback(async (request, options$1 = {}) => {
const { method, keepalive, headers, body, redirect, integrity, signal, credentials, mode, referrer, referrerPolicy, window } = options$1;
const id = nanoid(8);
const abortController = new AbortController();
let controller;
const currentAgent = agentRef.current;
localRequestIdsRef.current.add(id);
signal?.addEventListener("abort", () => {
currentAgent.send(JSON.stringify({
id,
type: MessageType.CF_AGENT_CHAT_REQUEST_CANCEL
}));
abortController.abort();
try {
controller.close();
} catch {}
localRequestIdsRef.current.delete(id);
});
currentAgent.addEventListener("message", (event) => {
let data;
try {
data = JSON.parse(event.data);
} catch (_error) {
return;
}
if (data.type === MessageType.CF_AGENT_USE_CHAT_RESPONSE) {
if (data.id === id) if (data.error) {
controller.error(new Error(data.body));
abortController.abort();
localRequestIdsRef.current.delete(id);
} else {
if (data.body?.trim()) controller.enqueue(new TextEncoder().encode(`data: ${data.body}\n\n`));
if (data.done) {
try {
controller.close();
} catch {}
abortController.abort();
localRequestIdsRef.current.delete(id);
}
}
}
}, { signal: abortController.signal });
const stream = new ReadableStream({
start(c) {
controller = c;
},
cancel(reason) {
console.warn("[agents/ai-react] cancelling stream", id, reason || "no reason");
}
});
currentAgent.send(JSON.stringify({
id,
init: {
body,
credentials,
headers,
integrity,
keepalive,
method,
mode,
redirect,
referrer,
referrerPolicy,
window
},
type: MessageType.CF_AGENT_USE_CHAT_REQUEST,
url: request.toString()
}));
return new Response(stream);
}, []);
const toolsRef = useRef(tools);
toolsRef.current = tools;
const prepareSendMessagesRequestRef = useRef(prepareSendMessagesRequest);
prepareSendMessagesRequestRef.current = prepareSendMessagesRequest;
const customTransport = useMemo(() => ({
sendMessages: async (sendMessageOptions) => {
const clientToolSchemas = extractClientToolSchemas(toolsRef.current);
return new DefaultChatTransport({
api: agentUrlString,
fetch: aiFetch,
prepareSendMessagesRequest: clientToolSchemas || prepareSendMessagesRequestRef.current ? async (prepareOptions) => {
let body = {};
let headers;
let credentials;
let api;
if (clientToolSchemas) body = {
id: prepareOptions.id,
messages: prepareOptions.messages,
trigger: prepareOptions.trigger,
clientTools: clientToolSchemas
};
if (prepareSendMessagesRequestRef.current) {
const userResult = await prepareSendMessagesRequestRef.current(prepareOptions);
headers = userResult.headers;
credentials = userResult.credentials;
api = userResult.api;
body = {
...body,
...userResult.body ?? {}
};
}
return {
body,
headers,
credentials,
api
};
} : void 0
}).sendMessages(sendMessageOptions);
},
reconnectToStream: async () => null
}), [agentUrlString, aiFetch]);
const useChatHelpers = useChat({
...rest,
messages: initialMessages,
transport: customTransport,
id: agent._pk
});
const processedToolCalls = useRef(/* @__PURE__ */ new Set());
const isResolvingToolsRef = useRef(false);
const [clientToolResults, setClientToolResults] = useState(/* @__PURE__ */ new Map());
const messagesRef = useRef(useChatHelpers.messages);
messagesRef.current = useChatHelpers.messages;
const lastMessage = useChatHelpers.messages[useChatHelpers.messages.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;
useEffect(() => {
if (!experimental_automaticToolResolution) return;
if (isResolvingToolsRef.current) return;
const lastMessage$1 = useChatHelpers.messages[useChatHelpers.messages.length - 1];
if (!lastMessage$1 || lastMessage$1.role !== "assistant") return;
const toolCalls = lastMessage$1.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$1 = currentTools?.[toolName];
if (tool$1?.execute && part.input !== void 0) try {
toolOutput = await tool$1.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) {
for (const result of toolResults) agentRef.current.send(JSON.stringify({
type: MessageType.CF_AGENT_TOOL_RESULT,
toolCallId: result.toolCallId,
toolName: result.toolName,
output: result.output,
autoContinue: autoContinueAfterToolResult
}));
await Promise.all(toolResults.map((result) => useChatHelpers.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;
});
}
} finally {
isResolvingToolsRef.current = false;
}
})();
}
}
}, [
useChatHelpers.messages,
experimental_automaticToolResolution,
useChatHelpers.addToolResult,
toolsRequiringConfirmation,
autoContinueAfterToolResult
]);
/**
* Contains the request ID, accumulated message parts, and a unique message ID.
* Used for both resumed streams and real-time broadcasts from other tabs.
*/
const activeStreamRef = useRef(null);
/**
* Tracks request IDs initiated by this tab via aiFetch.
* Used to distinguish local requests from broadcasts.
*/
const localRequestIdsRef = useRef(/* @__PURE__ */ new Set());
useEffect(() => {
/**
* 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 MessageType.CF_AGENT_CHAT_CLEAR:
useChatHelpers.setMessages([]);
break;
case MessageType.CF_AGENT_CHAT_MESSAGES:
useChatHelpers.setMessages(data.messages);
break;
case MessageType.CF_AGENT_MESSAGE_UPDATED:
useChatHelpers.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, updatedMessage];
});
break;
case MessageType.CF_AGENT_STREAM_RESUMING:
if (!resume) return;
activeStreamRef.current = null;
activeStreamRef.current = {
id: data.id,
messageId: nanoid(),
parts: []
};
agentRef.current.send(JSON.stringify({
type: MessageType.CF_AGENT_STREAM_RESUME_ACK,
id: data.id
}));
break;
case MessageType.CF_AGENT_USE_CHAT_RESPONSE: {
if (localRequestIdsRef.current.has(data.id)) return;
const isContinuation = data.continuation === true;
if (!activeStreamRef.current || activeStreamRef.current.id !== data.id) {
let messageId = nanoid();
let existingParts = [];
if (isContinuation) {
const currentMessages = messagesRef.current;
for (let i = currentMessages.length - 1; i >= 0; i--) if (currentMessages[i].role === "assistant") {
messageId = currentMessages[i].id;
existingParts = [...currentMessages[i].parts];
break;
}
}
activeStreamRef.current = {
id: data.id,
messageId,
parts: existingParts
};
}
const activeMsg = activeStreamRef.current;
if (data.body?.trim()) try {
const chunkData = JSON.parse(data.body);
switch (chunkData.type) {
case "text-start":
activeMsg.parts.push({
type: "text",
text: "",
state: "streaming"
});
break;
case "text-delta": {
const lastTextPart = [...activeMsg.parts].reverse().find((p) => p.type === "text");
if (lastTextPart && lastTextPart.type === "text") lastTextPart.text += chunkData.delta;
else activeMsg.parts.push({
type: "text",
text: chunkData.delta
});
break;
}
case "text-end": {
const lastTextPart = [...activeMsg.parts].reverse().find((p) => p.type === "text");
if (lastTextPart && "state" in lastTextPart) lastTextPart.state = "done";
break;
}
case "reasoning-start":
activeMsg.parts.push({
type: "reasoning",
text: "",
state: "streaming"
});
break;
case "reasoning-delta": {
const lastReasoningPart = [...activeMsg.parts].reverse().find((p) => p.type === "reasoning");
if (lastReasoningPart && lastReasoningPart.type === "reasoning") lastReasoningPart.text += chunkData.delta;
break;
}
case "reasoning-end": {
const lastReasoningPart = [...activeMsg.parts].reverse().find((p) => p.type === "reasoning");
if (lastReasoningPart && "state" in lastReasoningPart) lastReasoningPart.state = "done";
break;
}
case "file":
activeMsg.parts.push({
type: "file",
mediaType: chunkData.mediaType,
url: chunkData.url
});
break;
case "source-url":
activeMsg.parts.push({
type: "source-url",
sourceId: chunkData.sourceId,
url: chunkData.url,
title: chunkData.title
});
break;
case "source-document":
activeMsg.parts.push({
type: "source-document",
sourceId: chunkData.sourceId,
mediaType: chunkData.mediaType,
title: chunkData.title,
filename: chunkData.filename
});
break;
case "tool-input-available":
activeMsg.parts.push({
type: `tool-${chunkData.toolName}`,
toolCallId: chunkData.toolCallId,
toolName: chunkData.toolName,
state: "input-available",
input: chunkData.input
});
break;
case "tool-output-available":
activeMsg.parts = activeMsg.parts.map((p) => {
if ("toolCallId" in p && p.toolCallId === chunkData.toolCallId && "state" in p) return {
...p,
state: "output-available",
output: chunkData.output
};
return p;
});
break;
case "step-start":
activeMsg.parts.push({ type: "step-start" });
break;
}
useChatHelpers.setMessages((prevMessages) => {
if (!activeMsg) return prevMessages;
const existingIdx = prevMessages.findIndex((m) => m.id === activeMsg.messageId);
const partialMessage = {
id: activeMsg.messageId,
role: "assistant",
parts: [...activeMsg.parts]
};
if (existingIdx >= 0) {
const updated = [...prevMessages];
updated[existingIdx] = partialMessage;
return updated;
}
return [...prevMessages, partialMessage];
});
} 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.error) activeStreamRef.current = null;
break;
}
}
}
agent.addEventListener("message", onAgentMessage);
return () => {
agent.removeEventListener("message", onAgentMessage);
activeStreamRef.current = null;
};
}, [
agent,
useChatHelpers.setMessages,
resume
]);
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: MessageType.CF_AGENT_TOOL_RESULT,
toolCallId,
toolName,
output,
autoContinue: autoContinueAfterToolResult
}));
setClientToolResults((prev) => new Map(prev).set(toolCallId, output));
useChatHelpers.addToolResult(args);
if (!autoContinueAfterToolResult) {
if (!autoSendAfterAllConfirmationsResolved) {
useChatHelpers.sendMessage();
return;
}
const pending = pendingConfirmationsRef.current?.toolCallIds;
if (!pending) {
useChatHelpers.sendMessage();
return;
}
const wasLast = pending.size === 1 && pending.has(toolCallId);
if (pending.has(toolCallId)) pending.delete(toolCallId);
if (wasLast || pending.size === 0) useChatHelpers.sendMessage();
}
};
const messagesWithToolResults = useMemo(() => {
if (clientToolResults.size === 0) return useChatHelpers.messages;
return useChatHelpers.messages.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)
};
})
}));
}, [useChatHelpers.messages, clientToolResults]);
useEffect(() => {
const currentToolCallIds = /* @__PURE__ */ new Set();
for (const msg of useChatHelpers.messages) 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);
}, [useChatHelpers.messages]);
return {
...useChatHelpers,
messages: messagesWithToolResults,
addToolResult: addToolResultAndSendMessage,
clearHistory: () => {
useChatHelpers.setMessages([]);
setClientToolResults(/* @__PURE__ */ new Map());
processedToolCalls.current.clear();
agent.send(JSON.stringify({ type: MessageType.CF_AGENT_CHAT_CLEAR }));
},
setMessages: (messages) => {
useChatHelpers.setMessages(messages);
agent.send(JSON.stringify({
messages: Array.isArray(messages) ? messages : [],
type: MessageType.CF_AGENT_CHAT_MESSAGES
}));
}
};
}
//#endregion
export { detectToolsRequiringConfirmation, extractClientToolSchemas, useAgentChat };
//# sourceMappingURL=ai-react.js.map