Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/react-headless/src/store/createChatStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { subscribeWithSelector } from "zustand/middleware";
import { processStreamedMessage } from "../stream/processStreamedMessage";
import { identityMessageFormat } from "../types/messageFormat";
import type { ChatProviderProps, ChatStore, Message, Thread, UserMessage } from "./types";
import { safeRandomUUID } from "../utils/safe-random-uuid";

type StoreConfig = Omit<ChatProviderProps, "children">;

Expand Down Expand Up @@ -213,7 +214,7 @@ export const createChatStore = (config: StoreConfig) => {
const abortController = new AbortController();
const optimisticMessage: UserMessage = {
...message,
id: crypto.randomUUID(),
id: safeRandomUUID(),
role: "user",
};

Expand Down
5 changes: 3 additions & 2 deletions packages/react-headless/src/stream/adapters/langgraph.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AGUIEvent, EventType, StreamProtocolAdapter } from "../../types";
import { safeRandomUUID } from "../../utils/safe-random-uuid";

/**
* Represents a LangGraph AI message (or chunk) as received in the
Expand Down Expand Up @@ -66,7 +67,7 @@ export const langGraphAdapter = (options?: LangGraphAdapterOptions): StreamProto
if (!reader) throw new Error("No response body");

const decoder = new TextDecoder();
const messageId = crypto.randomUUID();
const messageId = safeRandomUUID();
const toolCallIds: Record<number, string> = {};
let messageStarted = false;
let buffer = "";
Expand Down Expand Up @@ -174,7 +175,7 @@ export const langGraphAdapter = (options?: LangGraphAdapterOptions): StreamProto
const tc = msg.tool_calls[i];
if (!tc) continue;

const toolCallId = tc.id || crypto.randomUUID();
const toolCallId = tc.id || safeRandomUUID();

// Only emit if we haven't already started this tool call
// via tool_call_chunks
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import type { ChatCompletionChunk } from "openai/resources/chat/completions";
import { AGUIEvent, EventType, StreamProtocolAdapter } from "../../types";
import { safeRandomUUID } from "../../utils/safe-random-uuid";

export const openAIAdapter = (): StreamProtocolAdapter => ({
async *parse(response: Response): AsyncIterable<AGUIEvent> {
const reader = response.body?.getReader();
if (!reader) throw new Error("No response body");

const decoder = new TextDecoder();
const messageId = crypto.randomUUID();
const messageId = safeRandomUUID();
const toolCallIds: Record<number, string> = {};
let messageStarted = false;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ChatCompletionChunk } from "openai/resources/chat/completions";
import { AGUIEvent, EventType, StreamProtocolAdapter } from "../../types";
import { safeRandomUUID } from "../../utils/safe-random-uuid";

/**
* Adapter for streams produced by the OpenAI SDK's `Stream.toReadableStream()`.
Expand All @@ -12,7 +13,7 @@ export const openAIReadableStreamAdapter = (): StreamProtocolAdapter => ({
if (!reader) throw new Error("No response body");

const decoder = new TextDecoder();
const messageId = crypto.randomUUID();
const messageId = safeRandomUUID();
const toolCallIds: Record<number, string> = {};
let messageStarted = false;
let buffer = "";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { AssistantMessage, Message, ToolMessage, UserMessage } from "../../types";
import type { MessageFormat } from "../../types/messageFormat";
import { safeRandomUUID } from "../../utils/safe-random-uuid";

// ── LangGraph / LangChain message types ──────────────────────────

Expand Down Expand Up @@ -61,7 +62,7 @@ function toLangChainMessage(message: Message): LangChainMessage {
// ── Inbound (LangGraph → AG-UI) ────────────────────────────────

function fromLangChainMessage(msg: LangChainMessage): Message {
const id = msg.id ?? crypto.randomUUID();
const id = msg.id ?? safeRandomUUID();

switch (msg.type) {
case "human":
Expand Down Expand Up @@ -139,7 +140,7 @@ function safeParseArgs(args: string): Record<string, unknown> | string {
* LangGraph → AG-UI (fromApi):
* - Maps `type` to `role` (`"human"` → `"user"`, `"ai"` → `"assistant"`)
* - Converts tool call `args` object to JSON string
* - Generates `id` via `crypto.randomUUID()` if not present
* - Generates `id` via `safeRandomUUID()` if not present
*/
export const langGraphMessageFormat: MessageFormat = {
toApi(messages: Message[]): LangChainMessage[] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
} from "openai/resources/responses/responses";
import type { AssistantMessage, Message, ToolMessage, UserMessage } from "../../types";
import type { MessageFormat } from "../../types/messageFormat";
import { safeRandomUUID } from "../../utils/safe-random-uuid";

// ── Outbound (AG-UI → OpenAI Responses/Conversations input) ─────

Expand Down Expand Up @@ -162,7 +163,7 @@ function fromItems(items: ConversationItem[]): Message[] {
const tc = item as { id?: string; call_id: string; name: string; arguments: string };

if (!currentAssistant) {
currentAssistant = { id: crypto.randomUUID(), role: "assistant" };
currentAssistant = { id: safeRandomUUID(), role: "assistant" };
}

currentAssistant = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
} from "openai/resources/chat/completions";
import type { AssistantMessage, Message, ToolMessage, UserMessage } from "../../types";
import type { MessageFormat } from "../../types/messageFormat";
import { safeRandomUUID } from "../../utils/safe-random-uuid";

// ── Outbound (AG-UI → OpenAI Completions) ───────────────────────

Expand Down Expand Up @@ -83,7 +84,7 @@ function fromOpenAIAssistant(msg: ChatCompletionAssistantMessageParam): Assistan
const content = typeof msg.content === "string" ? msg.content : undefined;

const result: AssistantMessage = {
id: crypto.randomUUID(),
id: safeRandomUUID(),
role: "assistant",
content,
};
Expand All @@ -106,23 +107,23 @@ function fromOpenAIAssistant(msg: ChatCompletionAssistantMessageParam): Assistan

function fromOpenAIUser(msg: ChatCompletionUserMessageParam): UserMessage {
if (typeof msg.content === "string") {
return { id: crypto.randomUUID(), role: "user", content: msg.content };
return { id: safeRandomUUID(), role: "user", content: msg.content };
}

const content = msg.content.map((part): { type: "text"; text: string } => {
if (part.type === "text") return { type: "text", text: part.text };
return { type: "text", text: "" };
});

return { id: crypto.randomUUID(), role: "user", content };
return { id: safeRandomUUID(), role: "user", content };
}

function fromOpenAITool(msg: ChatCompletionToolMessageParam): ToolMessage {
const content =
typeof msg.content === "string" ? msg.content : msg.content.map((p) => p.text).join("");

return {
id: crypto.randomUUID(),
id: safeRandomUUID(),
role: "tool",
content,
toolCallId: msg.tool_call_id,
Expand All @@ -139,18 +140,18 @@ function fromOpenAI(data: ChatCompletionMessageParam): Message {
return fromOpenAITool(data);
case "system":
return {
id: crypto.randomUUID(),
id: safeRandomUUID(),
role: "system",
content: typeof data.content === "string" ? data.content : "",
};
case "developer":
return {
id: crypto.randomUUID(),
id: safeRandomUUID(),
role: "developer",
content: typeof data.content === "string" ? data.content : "",
};
default:
return { id: crypto.randomUUID(), role: "system", content: "" };
return { id: safeRandomUUID(), role: "system", content: "" };
}
}

Expand All @@ -170,7 +171,7 @@ function fromOpenAI(data: ChatCompletionMessageParam): Message {
* - Converts multipart `content` arrays to OpenAI content format
*
* OpenAI → AG-UI (fromApi):
* - Generates `id` via `crypto.randomUUID()`
* - Generates `id` via `safeRandomUUID()`
* - Converts `tool_calls` → `toolCalls`
* - Converts `tool_call_id` → `toolCallId`
*/
Expand Down
3 changes: 2 additions & 1 deletion packages/react-headless/src/stream/processStreamedMessage.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { AssistantMessage, EventType, StreamProtocolAdapter } from "../types";
import { agUIAdapter } from "./adapters";
import { safeRandomUUID } from "../utils/safe-random-uuid";

/**
* @inline
Expand Down Expand Up @@ -27,7 +28,7 @@ export const processStreamedMessage = async ({
adapter = agUIAdapter(),
}: Parameters): Promise<AssistantMessage | void> => {
let currentMessage: AssistantMessage = {
id: crypto.randomUUID(),
id: safeRandomUUID(),
role: "assistant",
content: "",
toolCalls: [],
Expand Down
23 changes: 23 additions & 0 deletions packages/react-headless/src/utils/safe-random-uuid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Returns a UUID, falling back gracefully when `crypto.randomUUID` is unavailable.
*
* `crypto.randomUUID()` is only defined in a secure context — HTTPS, or
* `http://localhost` / `127.0.0.1` / `[::1]`. When the app is served from a
* non-secure origin (for example a LAN IP over plain HTTP, or `0.0.0.0`),
* `crypto.randomUUID` is `undefined` and calling it throws
* "crypto.randomUUID is not a function". This helper detects that case and
* falls back to a sufficiently-unique id, which is all that is needed for
* client-side message keys.
*/
export function safeRandomUUID(): string {
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
return crypto.randomUUID();
}
return (
"uid-" +
Date.now().toString(36) +
"-" +
Math.random().toString(36).slice(2, 10) +
Math.random().toString(36).slice(2, 10)
);
}
Loading