@ai-presence/adapters 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # @ai-presence/adapters
2
+
3
+ Starter adapter utilities for converting AI runtime events into AI Presence Kit events.
4
+
5
+ Adapters should stay upstream of any renderer. Their job is to translate signals like user input, stream open, first token, speech start, interruption, and errors into the canonical state machine exposed by `@ai-presence/core`.
6
+
7
+ Current adapters:
8
+
9
+ - `createRuntimeSignalAdapter`: generic signal-to-presence bridge.
10
+ - `createVercelAISDKAdapter`: maps AI SDK chat statuses and callbacks to presence.
11
+ - `createOpenAIRealtimeAdapter`: maps OpenAI Realtime server events to presence.
12
+ - `createChatEventAdapter`: maps small generic chat lifecycle events to presence.
13
+
14
+ The adapters intentionally avoid importing framework packages. They accept plain objects so they can be wrapped by React, Svelte, Vue, server streams, WebRTC handlers, or custom chat runtimes later.
15
+
16
+ ```js
17
+ import { createVercelAISDKAdapter } from "@ai-presence/adapters";
18
+
19
+ const aiSdkPresence = createVercelAISDKAdapter(presenceRuntime);
20
+ aiSdkPresence.update({ status: "streaming", messages: [] });
21
+ ```
22
+
23
+ Run the local adapter trace demo with:
24
+
25
+ ```bash
26
+ npm run demo:adapters
27
+ ```
28
+
29
+ The trace demo prints each adapter transition with the shared core control inputs, reference face frame evidence, and bounded six-channel decision-trace evidence that a renderer can consume, for example `phase=before-output`, `attention=response`, `face=thinking`, `channels=gaze,blink,brows,mouth,posture,motion`, `trace=complete`, `decisions=6`, `safe=true`, `warnings=0`, `transition=thinking:stream-open+0ms`, `transitionReads=6/6`, and `reads=state,transitionEvent,transitionAgeMs`.
30
+
31
+ ## Vercel AI SDK
32
+
33
+ The current AI SDK `useChat` status values are:
34
+
35
+ ```text
36
+ submitted
37
+ streaming
38
+ ready
39
+ error
40
+ ```
41
+
42
+ Adapter mapping:
43
+
44
+ ```text
45
+ submitted -> model-waiting -> thinking
46
+ streaming with no assistant content -> stream-open -> waiting
47
+ streaming with assistant content -> token -> streaming
48
+ ready -> response-complete -> ready
49
+ error -> error -> error
50
+ ```
51
+
52
+ This preserves the important distinction from the AI SDK docs and troubleshooting notes: `streaming` can begin before user-visible assistant text exists, so the presence state should be `waiting` until content arrives.
53
+
54
+ ## OpenAI Realtime
55
+
56
+ Adapter mapping:
57
+
58
+ ```text
59
+ input_audio_buffer.speech_started -> local-read -> reading
60
+ input_audio_buffer.speech_stopped -> model-waiting -> thinking
61
+ response.created -> model-waiting -> thinking
62
+ response.output_item.created -> stream-open -> waiting
63
+ response.content_part.added -> stream-open -> waiting
64
+ response.output_text.delta -> token -> streaming
65
+ response.output_audio_transcript.delta -> token -> streaming
66
+ response.output_audio.delta -> speech-start -> speaking
67
+ response.output_audio.done -> speech-end -> ready
68
+ response.done -> response-complete -> ready
69
+ error / response.failed -> error -> error
70
+ ```
71
+
72
+ Older alias events already used by the prototype, such as `response.audio.delta` and `response.text.delta`, are also accepted.
package/dist/index.mjs ADDED
@@ -0,0 +1,21 @@
1
+ import adapters from "../src/runtime-adapter.js";
2
+
3
+ export const VercelAIStatus = adapters.VercelAIStatus;
4
+ export const OPENAI_REALTIME_EVENT_MAP = adapters.OPENAI_REALTIME_EVENT_MAP;
5
+ export const RuntimeSignal = adapters.RuntimeSignal;
6
+ export const RUNTIME_SIGNALS = adapters.RUNTIME_SIGNALS;
7
+ export const applyRuntimeSignal = adapters.applyRuntimeSignal;
8
+ export const chatEventToRuntimeSignal = adapters.chatEventToRuntimeSignal;
9
+ export const createChatEventAdapter = adapters.createChatEventAdapter;
10
+ export const createOpenAIRealtimeAdapter = adapters.createOpenAIRealtimeAdapter;
11
+ export const createRuntimeSignalAdapter = adapters.createRuntimeSignalAdapter;
12
+ export const createVercelAISDKAdapter = adapters.createVercelAISDKAdapter;
13
+ export const isRuntimeSignal = adapters.isRuntimeSignal;
14
+ export const lastAssistantText = adapters.lastAssistantText;
15
+ export const normalizeRuntimeSignal = adapters.normalizeRuntimeSignal;
16
+ export const openAIRealtimeEventToRuntimeSignal = adapters.openAIRealtimeEventToRuntimeSignal;
17
+ export const presenceEventForRuntimeSignal = adapters.presenceEventForRuntimeSignal;
18
+ export const textFromMessage = adapters.textFromMessage;
19
+ export const vercelAIStatusToRuntimeSignal = adapters.vercelAIStatusToRuntimeSignal;
20
+
21
+ export default adapters;
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@ai-presence/adapters",
3
+ "version": "0.1.0",
4
+ "description": "Runtime signal adapters for AI Presence Kit.",
5
+ "type": "commonjs",
6
+ "main": "./src/runtime-adapter.js",
7
+ "module": "./dist/index.mjs",
8
+ "types": "./src/runtime-adapter.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./src/runtime-adapter.d.ts",
12
+ "import": "./dist/index.mjs",
13
+ "require": "./src/runtime-adapter.js",
14
+ "default": "./src/runtime-adapter.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "src",
20
+ "README.md"
21
+ ],
22
+ "dependencies": {
23
+ "@ai-presence/core": "0.1.0"
24
+ },
25
+ "sideEffects": false,
26
+ "license": "MIT"
27
+ }
@@ -0,0 +1,91 @@
1
+ import type { PresenceEventValue, PresenceRuntime, PresenceSnapshot } from "@ai-presence/core";
2
+
3
+ export declare const RuntimeSignal: Readonly<{
4
+ RESET: "reset";
5
+ USER_INPUT: "user-input";
6
+ USER_PAUSE: "user-pause";
7
+ LOCAL_READ: "local-read";
8
+ MODEL_WAITING: "model-waiting";
9
+ STREAM_OPEN: "stream-open";
10
+ TOKEN: "token";
11
+ RESPONSE_COMPLETE: "response-complete";
12
+ SPEECH_START: "speech-start";
13
+ SPEECH_END: "speech-end";
14
+ VOICE_WAITING: "voice-waiting";
15
+ INTERRUPT: "interrupt";
16
+ ERROR: "error";
17
+ }>;
18
+
19
+ export type RuntimeSignalValue = typeof RuntimeSignal[keyof typeof RuntimeSignal];
20
+
21
+ export declare const VercelAIStatus: Readonly<{
22
+ SUBMITTED: "submitted";
23
+ STREAMING: "streaming";
24
+ READY: "ready";
25
+ ERROR: "error";
26
+ }>;
27
+
28
+ export type VercelAIStatusValue = typeof VercelAIStatus[keyof typeof VercelAIStatus];
29
+
30
+ export interface RuntimeSignalObject {
31
+ type: RuntimeSignalValue;
32
+ detail?: Record<string, unknown>;
33
+ [key: string]: unknown;
34
+ }
35
+
36
+ export interface NormalizedRuntimeSignal {
37
+ type: RuntimeSignalValue;
38
+ detail: Record<string, unknown>;
39
+ }
40
+
41
+ export interface RuntimeSignalAdapter {
42
+ send(signal: RuntimeSignalValue | RuntimeSignalObject): PresenceSnapshot;
43
+ }
44
+
45
+ export interface AdapterOptions {
46
+ onSignal?: (signal: NormalizedRuntimeSignal, snapshot: PresenceSnapshot) => void;
47
+ }
48
+
49
+ export interface VercelChatState {
50
+ status: VercelAIStatusValue | string;
51
+ messages?: Array<Record<string, unknown>>;
52
+ assistantText?: string;
53
+ completion?: string;
54
+ [key: string]: unknown;
55
+ }
56
+
57
+ export interface VercelAISDKAdapter {
58
+ onInput(text: string, detail?: Record<string, unknown>): PresenceSnapshot;
59
+ onSubmit(text: string, detail?: Record<string, unknown>): PresenceSnapshot;
60
+ update(chatState: VercelChatState | VercelAIStatusValue): PresenceSnapshot;
61
+ onData(dataPart: Record<string, unknown>, detail?: Record<string, unknown>): PresenceSnapshot;
62
+ onFinish(result?: Record<string, unknown>): PresenceSnapshot;
63
+ onError(error: unknown, detail?: Record<string, unknown>): PresenceSnapshot;
64
+ }
65
+
66
+ export interface EventAdapter {
67
+ handleEvent(event: string | Record<string, unknown>): PresenceSnapshot;
68
+ }
69
+
70
+ export declare const RUNTIME_SIGNALS: readonly RuntimeSignalValue[];
71
+ export declare const OPENAI_REALTIME_EVENT_MAP: Readonly<Record<string, RuntimeSignalValue>>;
72
+
73
+ export declare function applyRuntimeSignal(
74
+ presenceRuntime: PresenceRuntime,
75
+ signal: RuntimeSignalValue | RuntimeSignalObject,
76
+ ): PresenceSnapshot;
77
+ export declare function chatEventToRuntimeSignal(event?: string | Record<string, unknown>): NormalizedRuntimeSignal;
78
+ export declare function createChatEventAdapter(presenceRuntime: PresenceRuntime, options?: AdapterOptions): EventAdapter;
79
+ export declare function createOpenAIRealtimeAdapter(presenceRuntime: PresenceRuntime, options?: AdapterOptions): EventAdapter;
80
+ export declare function createRuntimeSignalAdapter(presenceRuntime: PresenceRuntime, options?: AdapterOptions): RuntimeSignalAdapter;
81
+ export declare function createVercelAISDKAdapter(presenceRuntime: PresenceRuntime, options?: AdapterOptions): VercelAISDKAdapter;
82
+ export declare function isRuntimeSignal(value: unknown): value is RuntimeSignalValue;
83
+ export declare function lastAssistantText(messages?: Array<Record<string, unknown>>): string;
84
+ export declare function normalizeRuntimeSignal(signal: RuntimeSignalValue | RuntimeSignalObject): NormalizedRuntimeSignal;
85
+ export declare function openAIRealtimeEventToRuntimeSignal(event?: string | Record<string, unknown>): NormalizedRuntimeSignal;
86
+ export declare function presenceEventForRuntimeSignal(signal: RuntimeSignalValue | RuntimeSignalObject): {
87
+ event: PresenceEventValue;
88
+ detail: Record<string, unknown>;
89
+ };
90
+ export declare function textFromMessage(message: Record<string, unknown>): string;
91
+ export declare function vercelAIStatusToRuntimeSignal(chatState?: VercelChatState | VercelAIStatusValue): NormalizedRuntimeSignal;
@@ -0,0 +1,343 @@
1
+ (function initPresenceAdapters(globalScope) {
2
+ "use strict";
3
+
4
+ const core = resolveCore(globalScope);
5
+ const PresenceEvent = core?.PresenceEvent || {};
6
+
7
+ const RuntimeSignal = Object.freeze({
8
+ RESET: "reset",
9
+ USER_INPUT: "user-input",
10
+ USER_PAUSE: "user-pause",
11
+ LOCAL_READ: "local-read",
12
+ MODEL_WAITING: "model-waiting",
13
+ STREAM_OPEN: "stream-open",
14
+ TOKEN: "token",
15
+ RESPONSE_COMPLETE: "response-complete",
16
+ SPEECH_START: "speech-start",
17
+ SPEECH_END: "speech-end",
18
+ VOICE_WAITING: "voice-waiting",
19
+ INTERRUPT: "interrupt",
20
+ ERROR: "error",
21
+ });
22
+
23
+ const RUNTIME_SIGNALS = Object.freeze(Object.values(RuntimeSignal));
24
+ const runtimeSignalSet = new Set(RUNTIME_SIGNALS);
25
+
26
+ const signalToPresenceEvent = Object.freeze({
27
+ [RuntimeSignal.RESET]: PresenceEvent.RESET,
28
+ [RuntimeSignal.USER_INPUT]: PresenceEvent.USER_INPUT,
29
+ [RuntimeSignal.USER_PAUSE]: PresenceEvent.USER_PAUSE,
30
+ [RuntimeSignal.LOCAL_READ]: PresenceEvent.LOCAL_READ,
31
+ [RuntimeSignal.MODEL_WAITING]: PresenceEvent.SUBMIT,
32
+ [RuntimeSignal.STREAM_OPEN]: PresenceEvent.STREAM_OPEN,
33
+ [RuntimeSignal.TOKEN]: PresenceEvent.TOKEN,
34
+ [RuntimeSignal.RESPONSE_COMPLETE]: PresenceEvent.RESPONSE_COMPLETE,
35
+ [RuntimeSignal.SPEECH_START]: PresenceEvent.SPEECH_START,
36
+ [RuntimeSignal.SPEECH_END]: PresenceEvent.SPEECH_END,
37
+ [RuntimeSignal.VOICE_WAITING]: PresenceEvent.VOICE_WAITING,
38
+ [RuntimeSignal.INTERRUPT]: PresenceEvent.INTERRUPT,
39
+ [RuntimeSignal.ERROR]: PresenceEvent.ERROR,
40
+ });
41
+
42
+ const VercelAIStatus = Object.freeze({
43
+ SUBMITTED: "submitted",
44
+ STREAMING: "streaming",
45
+ READY: "ready",
46
+ ERROR: "error",
47
+ });
48
+
49
+ const OPENAI_REALTIME_EVENT_MAP = Object.freeze({
50
+ "session.created": RuntimeSignal.VOICE_WAITING,
51
+ "session.updated": RuntimeSignal.VOICE_WAITING,
52
+ "input_audio_buffer.speech_started": RuntimeSignal.LOCAL_READ,
53
+ "input_audio_buffer.speech_stopped": RuntimeSignal.MODEL_WAITING,
54
+ "input_audio_buffer.committed": RuntimeSignal.MODEL_WAITING,
55
+ "response.created": RuntimeSignal.MODEL_WAITING,
56
+ "response.output_item.created": RuntimeSignal.STREAM_OPEN,
57
+ "response.content_part.added": RuntimeSignal.STREAM_OPEN,
58
+ "response.output_text.delta": RuntimeSignal.TOKEN,
59
+ "response.text.delta": RuntimeSignal.TOKEN,
60
+ "response.output_audio_transcript.delta": RuntimeSignal.TOKEN,
61
+ "response.audio_transcript.delta": RuntimeSignal.TOKEN,
62
+ "response.output_audio.delta": RuntimeSignal.SPEECH_START,
63
+ "response.audio.delta": RuntimeSignal.SPEECH_START,
64
+ "response.output_audio.done": RuntimeSignal.SPEECH_END,
65
+ "response.audio.done": RuntimeSignal.SPEECH_END,
66
+ "response.output_text.done": RuntimeSignal.RESPONSE_COMPLETE,
67
+ "response.text.done": RuntimeSignal.RESPONSE_COMPLETE,
68
+ "response.output_audio_transcript.done": RuntimeSignal.RESPONSE_COMPLETE,
69
+ "response.audio_transcript.done": RuntimeSignal.RESPONSE_COMPLETE,
70
+ "response.done": RuntimeSignal.RESPONSE_COMPLETE,
71
+ "response.completed": RuntimeSignal.RESPONSE_COMPLETE,
72
+ "response.failed": RuntimeSignal.ERROR,
73
+ error: RuntimeSignal.ERROR,
74
+ });
75
+
76
+ function resolveCore(scope) {
77
+ if (scope?.AIPresenceCore) return scope.AIPresenceCore;
78
+ if (typeof require === "function") {
79
+ try {
80
+ return require("@ai-presence/core");
81
+ } catch {
82
+ // Fall back to the no-build monorepo path used by the prototype.
83
+ }
84
+ try {
85
+ return require("../../core/src/presence-core.js");
86
+ } catch {
87
+ return null;
88
+ }
89
+ }
90
+ return null;
91
+ }
92
+
93
+ function isRuntimeSignal(value) {
94
+ return runtimeSignalSet.has(value);
95
+ }
96
+
97
+ function normalizeRuntimeSignal(signal) {
98
+ if (typeof signal === "string") {
99
+ return { type: signal, detail: {} };
100
+ }
101
+
102
+ if (!signal || typeof signal !== "object") {
103
+ return { type: RuntimeSignal.RESET, detail: {} };
104
+ }
105
+
106
+ const { type, detail: explicitDetail, ...rest } = signal;
107
+ const detail = explicitDetail && typeof explicitDetail === "object"
108
+ ? { ...rest, ...explicitDetail }
109
+ : rest;
110
+
111
+ return {
112
+ type: isRuntimeSignal(type) ? type : RuntimeSignal.RESET,
113
+ detail,
114
+ };
115
+ }
116
+
117
+ function presenceEventForRuntimeSignal(signal) {
118
+ const normalized = normalizeRuntimeSignal(signal);
119
+ return {
120
+ event: signalToPresenceEvent[normalized.type] || PresenceEvent.RESET,
121
+ detail: normalized.detail,
122
+ };
123
+ }
124
+
125
+ function textFromPart(part) {
126
+ if (!part || typeof part !== "object") return "";
127
+ if (typeof part.text === "string") return part.text;
128
+ if (typeof part.content === "string") return part.content;
129
+ if (typeof part.delta === "string") return part.delta;
130
+ return "";
131
+ }
132
+
133
+ function textFromMessage(message) {
134
+ if (!message || typeof message !== "object") return "";
135
+ if (typeof message.content === "string") return message.content;
136
+ if (typeof message.text === "string") return message.text;
137
+ if (!Array.isArray(message.parts)) return "";
138
+ return message.parts.map(textFromPart).join("");
139
+ }
140
+
141
+ function lastAssistantText(messages = []) {
142
+ if (!Array.isArray(messages)) return "";
143
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
144
+ const message = messages[index];
145
+ if (message?.role === "assistant") return textFromMessage(message);
146
+ }
147
+ return "";
148
+ }
149
+
150
+ function hasAssistantContent(chatState = {}) {
151
+ if (typeof chatState.assistantText === "string") return chatState.assistantText.length > 0;
152
+ if (typeof chatState.completion === "string") return chatState.completion.length > 0;
153
+ return lastAssistantText(chatState.messages).length > 0;
154
+ }
155
+
156
+ function vercelAIStatusToRuntimeSignal(chatState = {}) {
157
+ const status = typeof chatState === "string" ? chatState : chatState.status;
158
+ const detail = typeof chatState === "string" ? {} : { ...chatState };
159
+
160
+ if (status === VercelAIStatus.SUBMITTED) {
161
+ return { type: RuntimeSignal.MODEL_WAITING, detail };
162
+ }
163
+
164
+ if (status === VercelAIStatus.STREAMING) {
165
+ return {
166
+ type: hasAssistantContent(detail) ? RuntimeSignal.TOKEN : RuntimeSignal.STREAM_OPEN,
167
+ detail,
168
+ };
169
+ }
170
+
171
+ if (status === VercelAIStatus.READY) {
172
+ return { type: RuntimeSignal.RESPONSE_COMPLETE, detail };
173
+ }
174
+
175
+ if (status === VercelAIStatus.ERROR) {
176
+ return { type: RuntimeSignal.ERROR, detail };
177
+ }
178
+
179
+ return { type: RuntimeSignal.RESET, detail };
180
+ }
181
+
182
+ function createVercelAISDKAdapter(presenceRuntime, options = {}) {
183
+ const adapter = createRuntimeSignalAdapter(presenceRuntime, options);
184
+
185
+ return Object.freeze({
186
+ onInput(text, detail = {}) {
187
+ return adapter.send({ type: RuntimeSignal.USER_INPUT, text, ...detail });
188
+ },
189
+ onSubmit(text, detail = {}) {
190
+ adapter.send({ type: RuntimeSignal.USER_INPUT, text, ...detail });
191
+ return adapter.send({ type: RuntimeSignal.MODEL_WAITING, text, ...detail });
192
+ },
193
+ update(chatState) {
194
+ return adapter.send(vercelAIStatusToRuntimeSignal(chatState));
195
+ },
196
+ onData(dataPart, detail = {}) {
197
+ return adapter.send({
198
+ type: hasAssistantContent({ assistantText: textFromPart(dataPart) })
199
+ ? RuntimeSignal.TOKEN
200
+ : RuntimeSignal.STREAM_OPEN,
201
+ dataPart,
202
+ ...detail,
203
+ });
204
+ },
205
+ onFinish(result = {}) {
206
+ if (result.isAbort) return adapter.send({ type: RuntimeSignal.INTERRUPT, ...result });
207
+ if (result.isError) return adapter.send({ type: RuntimeSignal.ERROR, ...result });
208
+ return adapter.send({ type: RuntimeSignal.RESPONSE_COMPLETE, ...result });
209
+ },
210
+ onError(error, detail = {}) {
211
+ return adapter.send({ type: RuntimeSignal.ERROR, error, ...detail });
212
+ },
213
+ });
214
+ }
215
+
216
+ function openAIRealtimeEventToRuntimeSignal(event = {}) {
217
+ const type = typeof event === "string" ? event : event.type;
218
+ const signalType = OPENAI_REALTIME_EVENT_MAP[type] || RuntimeSignal.RESET;
219
+ const detail = typeof event === "string" ? { eventType: event } : { ...event, eventType: type };
220
+
221
+ if (type === "input_audio_buffer.speech_started") {
222
+ detail.text = detail.text || "voice input";
223
+ detail.completion = 0.2;
224
+ }
225
+
226
+ if (type === "input_audio_buffer.speech_stopped" || type === "input_audio_buffer.committed") {
227
+ detail.text = detail.text || "voice input";
228
+ }
229
+
230
+ if (type?.endsWith(".delta")) {
231
+ detail.delta = detail.delta || detail.text || "";
232
+ }
233
+
234
+ return { type: signalType, detail };
235
+ }
236
+
237
+ function createOpenAIRealtimeAdapter(presenceRuntime, options = {}) {
238
+ const adapter = createRuntimeSignalAdapter(presenceRuntime, options);
239
+
240
+ return Object.freeze({
241
+ handleEvent(event) {
242
+ return adapter.send(openAIRealtimeEventToRuntimeSignal(event));
243
+ },
244
+ });
245
+ }
246
+
247
+ function chatEventToRuntimeSignal(event = {}) {
248
+ const type = typeof event === "string" ? event : event.type;
249
+ const detail = typeof event === "string" ? {} : { ...event };
250
+
251
+ switch (type) {
252
+ case "input":
253
+ case "user-input":
254
+ return { type: RuntimeSignal.USER_INPUT, detail };
255
+ case "pause":
256
+ case "user-pause":
257
+ return { type: RuntimeSignal.USER_PAUSE, detail };
258
+ case "submit":
259
+ case "request":
260
+ return { type: RuntimeSignal.MODEL_WAITING, detail };
261
+ case "stream-open":
262
+ case "response-start":
263
+ return { type: RuntimeSignal.STREAM_OPEN, detail };
264
+ case "delta":
265
+ case "token":
266
+ case "message-delta":
267
+ return { type: RuntimeSignal.TOKEN, detail };
268
+ case "finish":
269
+ case "done":
270
+ case "response-complete":
271
+ return { type: RuntimeSignal.RESPONSE_COMPLETE, detail };
272
+ case "abort":
273
+ case "interrupt":
274
+ return { type: RuntimeSignal.INTERRUPT, detail };
275
+ case "speech-start":
276
+ return { type: RuntimeSignal.SPEECH_START, detail };
277
+ case "speech-end":
278
+ return { type: RuntimeSignal.SPEECH_END, detail };
279
+ case "error":
280
+ return { type: RuntimeSignal.ERROR, detail };
281
+ default:
282
+ return { type: RuntimeSignal.RESET, detail };
283
+ }
284
+ }
285
+
286
+ function createChatEventAdapter(presenceRuntime, options = {}) {
287
+ const adapter = createRuntimeSignalAdapter(presenceRuntime, options);
288
+
289
+ return Object.freeze({
290
+ handleEvent(event) {
291
+ return adapter.send(chatEventToRuntimeSignal(event));
292
+ },
293
+ });
294
+ }
295
+
296
+ function applyRuntimeSignal(presenceRuntime, signal) {
297
+ if (!presenceRuntime || typeof presenceRuntime.send !== "function") {
298
+ throw new TypeError("applyRuntimeSignal requires a presence runtime with send(event, detail).");
299
+ }
300
+
301
+ const { event, detail } = presenceEventForRuntimeSignal(signal);
302
+ return presenceRuntime.send(event, detail);
303
+ }
304
+
305
+ function createRuntimeSignalAdapter(presenceRuntime, options = {}) {
306
+ const onSignal = typeof options.onSignal === "function" ? options.onSignal : null;
307
+
308
+ return Object.freeze({
309
+ send(signal) {
310
+ const normalized = normalizeRuntimeSignal(signal);
311
+ const result = applyRuntimeSignal(presenceRuntime, normalized);
312
+ if (onSignal) onSignal(normalized, result);
313
+ return result;
314
+ },
315
+ });
316
+ }
317
+
318
+ const api = Object.freeze({
319
+ VercelAIStatus,
320
+ OPENAI_REALTIME_EVENT_MAP,
321
+ RuntimeSignal,
322
+ RUNTIME_SIGNALS,
323
+ applyRuntimeSignal,
324
+ chatEventToRuntimeSignal,
325
+ createChatEventAdapter,
326
+ createOpenAIRealtimeAdapter,
327
+ createRuntimeSignalAdapter,
328
+ createVercelAISDKAdapter,
329
+ isRuntimeSignal,
330
+ lastAssistantText,
331
+ normalizeRuntimeSignal,
332
+ openAIRealtimeEventToRuntimeSignal,
333
+ presenceEventForRuntimeSignal,
334
+ textFromMessage,
335
+ vercelAIStatusToRuntimeSignal,
336
+ });
337
+
338
+ if (typeof module === "object" && module.exports) {
339
+ module.exports = api;
340
+ }
341
+
342
+ globalScope.AIPresenceAdapters = api;
343
+ })(typeof globalThis !== "undefined" ? globalThis : window);