Skip to content

Fix #133 Add React Native Support #193

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
6 changes: 6 additions & 0 deletions .changeset/tame-beds-shake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@openai/agents-realtime': minor
'@openai/agents-core': minor
---

Add React Native platform support
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@ The OpenAI Agents SDK is a lightweight yet powerful framework for building multi

<img src="https://cdn.openai.com/API/docs/images/orchestration.png" alt="Image of the Agents Tracing UI" style="max-height: 803px;">


> [!NOTE]
> Looking for the Python version? Check out [Agents SDK Python](https://github.com/openai/openai-agents-python).


## Core concepts

1. **Agents**: LLMs configured with instructions, tools, guardrails, and handoffs.
Expand Down Expand Up @@ -43,6 +41,7 @@ Explore the [`examples/`](examples/) directory to see the SDK in action.
- Node.js 22 or later
- Deno
- Bun
- React Native

Experimental support:

Expand Down
1 change: 1 addition & 0 deletions docs/src/content/docs/guides/troubleshooting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ The OpenAI Agents SDK is supported on the following server environments:
- Node.js 22+
- Deno 2.35+
- Bun 1.2.5+
- React Native 0.79+

### Limited support

Expand Down
12 changes: 11 additions & 1 deletion packages/agents-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@
"types": "./dist/shims/shims-node.d.ts",
"default": "./dist/shims/shims-node.mjs"
},
"react-native": {
"require": "./dist/shims/shims-react-native.js",
"types": "./dist/shims/shims-react-native.d.ts",
"default": "./dist/shims/shims-react-native.mjs"
},
"require": {
"types": "./dist/shims/shims-node.d.ts",
"default": "./dist/shims/shims-node.js"
Expand All @@ -89,8 +94,12 @@
},
"dependencies": {
"@openai/zod": "npm:zod@^3.25.40",
"@ungap/structured-clone": "^1.3.0",
"debug": "^4.4.0",
"openai": "^5.0.1"
"event-target-shim": "^6.0.2",
"events": "^3.3.0",
"openai": "^5.0.1",
"react-native-uuid": "^2.0.3"
},
"peerDependencies": {
"zod": "^3.25.40"
Expand Down Expand Up @@ -121,6 +130,7 @@
},
"devDependencies": {
"@types/debug": "^4.1.12",
"@types/ungap__structured-clone": "^1.2.0",
"zod": "^3.25.40"
},
"files": [
Expand Down
105 changes: 105 additions & 0 deletions packages/agents-core/src/shims/shims-react-native.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/// <reference lib="dom" />
export { EventEmitter, EventEmitterEvents } from './interface';
import type { EventEmitterEvents, Timeout, Timer } from './interface';

import { EventEmitter as NodeEventEmitter } from 'events';
import 'event-target-shim';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need EventTarget as a shim in React Native?

import structuredClone from '@ungap/structured-clone';
import uuid from 'react-native-uuid';

if (!('structuredClone' in globalThis)) {
// @ts-expect-error - This is the recommended approach from ungap/structured-clone
globalThis.structuredClone = structuredClone;
}
export const randomUUID = (): string => uuid.v4();

export function loadEnv(): Record<string, string | undefined> {
return {};
}

export class ReactNativeEventEmitter<
Events extends EventEmitterEvents = Record<string, any[]>,
> extends NodeEventEmitter {
override on<K extends keyof Events & (string | symbol)>(
type: K,
listener: (...args: Events[K]) => void,
): this {
// Node's typings accept string | symbol; cast is safe.
return super.on(type as string | symbol, listener);
}

override off<K extends keyof Events & (string | symbol)>(
type: K,
listener: (...args: Events[K]) => void,
): this {
return super.off(type as string | symbol, listener);
}

override emit<K extends keyof Events & (string | symbol)>(
type: K,
...args: Events[K]
): boolean {
return super.emit(type as string | symbol, ...args);
}

override once<K extends keyof Events & (string | symbol)>(
type: K,
listener: (...args: Events[K]) => void,
): this {
return super.once(type as string | symbol, listener);
}
}

export { ReactNativeEventEmitter as RuntimeEventEmitter };

// Streams – placeholders (unused by the SDK on RN)
export const Readable = class {};
export const ReadableStream = globalThis.ReadableStream;
export const ReadableStreamController =
globalThis.ReadableStreamDefaultController;
export const TransformStream = globalThis.TransformStream;

export class AsyncLocalStorage {
#ctx: unknown = null;

run<T>(store: T, fn: () => unknown) {
this.#ctx = store;
return fn();
}
getStore<T>() {
return this.#ctx as T;
}
enterWith<T>(store: T) {
this.#ctx = store;
}
}

export function isBrowserEnvironment(): boolean {
return true;
}

export function isTracingLoopRunningByDefault(): boolean {
return false;
}

/* MCP not supported on mobile; export browser stubs */
export { MCPServerStdio, MCPServerStreamableHttp } from './mcp-server/browser';

class RNTimer implements Timer {
setTimeout(cb: () => void, ms: number): Timeout {
const id: any = setTimeout(cb, ms);
// RN timers don’t expose ref/unref; shim them
id.ref ??= () => id;
id.unref ??= () => id;
id.hasRef ??= () => true;
id.refresh ??= () => id;
return id;
}

clearTimeout(id: Timeout | string | number | undefined) {
clearTimeout(id as number);
}
}

const timer = new RNTimer();
export { timer };
10 changes: 9 additions & 1 deletion packages/agents-realtime/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@
"types": "./dist/shims/shims-browser.d.ts",
"default": "./dist/shims/shims-browser.mjs"
},
"react-native": {
"require": "./dist/shims/shims-react-native.js",
"types": "./dist/shims/shims-react-native.d.ts",
"default": "./dist/shims/shims-react-native.mjs"
},
"node": {
"require": "./dist/shims/shims-node.js",
"types": "./dist/shims/shims-node.d.ts",
Expand Down Expand Up @@ -79,5 +84,8 @@
},
"files": [
"dist"
]
],
"peerDependencies": {
"react-native-webrtc": "^124.0.5"
}
}
28 changes: 16 additions & 12 deletions packages/agents-realtime/src/openaiRealtimeWebRtc.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
/// <reference lib="dom" />

import { isBrowserEnvironment } from '@openai/agents-core/_shims';
import {
isBrowserEnvironment,
mediaDevices as shimMediaDevices,
RTCPeerConnection as RTCPeerConnectionCtor,
} from '@openai/agents-realtime/_shims';
import {
RealtimeTransportLayer,
RealtimeTransportLayerConnectOptions,
Expand Down Expand Up @@ -94,7 +98,7 @@ export class OpenAIRealtimeWebRTC
#muted = false;

constructor(private readonly options: OpenAIRealtimeWebRTCOptions = {}) {
if (typeof RTCPeerConnection === 'undefined') {
if (typeof RTCPeerConnectionCtor === 'undefined') {
throw new Error('WebRTC is not supported in this environment');
}
super(options);
Expand Down Expand Up @@ -166,7 +170,7 @@ export class OpenAIRealtimeWebRTC

const connectionUrl = new URL(baseUrl);

let peerConnection: RTCPeerConnection = new RTCPeerConnection();
let peerConnection: RTCPeerConnection = new RTCPeerConnectionCtor();
const dataChannel = peerConnection.createDataChannel('oai-events');

this.#state = {
Expand Down Expand Up @@ -219,19 +223,19 @@ export class OpenAIRealtimeWebRTC
});

// set up audio playback
const audioElement =
this.options.audioElement ?? document.createElement('audio');
audioElement.autoplay = true;
peerConnection.ontrack = (event) => {
audioElement.srcObject = event.streams[0];
};
if (isBrowserEnvironment()) {
const audioElement =
this.options.audioElement ?? document.createElement('audio');
audioElement.autoplay = true;
peerConnection.ontrack = (event) => {
audioElement.srcObject = event.streams[0];
};
}

// get microphone stream
const stream =
this.options.mediaStream ??
(await navigator.mediaDevices.getUserMedia({
audio: true,
}));
(await shimMediaDevices.getUserMedia({ audio: true }));
peerConnection.addTrack(stream.getAudioTracks()[0]);

if (this.options.changePeerConnection) {
Expand Down
7 changes: 7 additions & 0 deletions packages/agents-realtime/src/shims/shims-browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,10 @@ export function isBrowserEnvironment(): boolean {
return true;
}
export const useWebSocketProtocols = true;

export const RTCPeerConnection = globalThis.RTCPeerConnection;
export const RTCIceCandidate = globalThis.RTCIceCandidate;
export const RTCSessionDescription = globalThis.RTCSessionDescription;
export const MediaStream = globalThis.MediaStream;
export const MediaStreamTrack = globalThis.MediaStreamTrack;
export const mediaDevices = navigator.mediaDevices;
12 changes: 12 additions & 0 deletions packages/agents-realtime/src/shims/shims-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,15 @@ export function isBrowserEnvironment(): boolean {
return false;
}
export const useWebSocketProtocols = false;

export const RTCPeerConnection =
undefined as unknown as typeof globalThis.RTCPeerConnection;
export const RTCIceCandidate =
undefined as unknown as typeof globalThis.RTCIceCandidate;
export const RTCSessionDescription =
undefined as unknown as typeof globalThis.RTCSessionDescription;
export const MediaStream =
undefined as unknown as typeof globalThis.MediaStream;
export const MediaStreamTrack =
undefined as unknown as typeof globalThis.MediaStreamTrack;
export const mediaDevices = undefined as unknown as MediaDevices;
24 changes: 24 additions & 0 deletions packages/agents-realtime/src/shims/shims-react-native.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {
RTCPeerConnection,
RTCIceCandidate,
RTCSessionDescription,
MediaStream,
MediaStreamTrack,
mediaDevices,
registerGlobals,
} from 'react-native-webrtc';

registerGlobals();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we are using the globals which is good so I would say let's drop the registerGlobals() that way it's still up to the implementor if they want to register them for additional use cases rather than us magically importing them


export const WebSocket = global.WebSocket;
export const isBrowserEnvironment = (): boolean => false;
export const useWebSocketProtocols = true;

export {
RTCPeerConnection,
RTCIceCandidate,
RTCSessionDescription,
MediaStream,
MediaStreamTrack,
mediaDevices,
};
12 changes: 12 additions & 0 deletions packages/agents-realtime/src/shims/shims-workerd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,15 @@ export function isBrowserEnvironment(): boolean {
return false;
}
export const useWebSocketProtocols = true;

export const RTCPeerConnection =
undefined as unknown as typeof globalThis.RTCPeerConnection;
export const RTCIceCandidate =
undefined as unknown as typeof globalThis.RTCIceCandidate;
export const RTCSessionDescription =
undefined as unknown as typeof globalThis.RTCSessionDescription;
export const MediaStream =
undefined as unknown as typeof globalThis.MediaStream;
export const MediaStreamTrack =
undefined as unknown as typeof globalThis.MediaStreamTrack;
export const mediaDevices = undefined as unknown as MediaDevices;
Loading