crowdplaysdk 0.4.2 → 0.5.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.
@@ -52,6 +52,17 @@ final class CrowdPlayRNCore {
52
52
  ])
53
53
  }.store(in: &cancellables)
54
54
 
55
+ // Agent tool calls (D-103): the JS layer answers via
56
+ // respondToAgentToolCall, so the native auto-handler stays unset.
57
+ engine.$latestAgentToolCall
58
+ .compactMap { $0 }
59
+ .sink { [weak self] call in
60
+ self?.send("crowdplay:agentToolCall", [
61
+ "callId": call.callId, "name": call.name,
62
+ "argumentsJSON": call.argumentsJSON,
63
+ ])
64
+ }.store(in: &cancellables)
65
+
55
66
  engine.$uploads
56
67
  .combineLatest(engine.$uploadsOnWiFi)
57
68
  .sink { [weak self] uploads, onWifi in
@@ -220,7 +231,7 @@ public final class CrowdPlayRNModule: RCTEventEmitter {
220
231
  public override static func requiresMainQueueSetup() -> Bool { true }
221
232
 
222
233
  public override func supportedEvents() -> [String]! {
223
- ["crowdplay:phase", "crowdplay:recording", "crowdplay:uploads", "crowdplay:participants", "crowdplay:warning", "crowdplay:audioRoute", "crowdplay:voiceActivity", "crowdplay:caption"]
234
+ ["crowdplay:phase", "crowdplay:recording", "crowdplay:uploads", "crowdplay:participants", "crowdplay:warning", "crowdplay:audioRoute", "crowdplay:voiceActivity", "crowdplay:caption", "crowdplay:agentToolCall"]
224
235
  }
225
236
 
226
237
  @objc public func startVoiceActivityUpdates() {
@@ -294,8 +305,30 @@ public final class CrowdPlayRNModule: RCTEventEmitter {
294
305
  let voice = agent["voice"] as? String
295
306
  let prompt = agent["systemPrompt"] as? String
296
307
  let context = agent["context"] as? String
297
- if voice != nil || prompt != nil || context != nil {
298
- agentOptions = CrowdPlayAgentOptions(voice: voice, systemPrompt: prompt, context: context)
308
+ // Tools cross the bridge as one JSON string (D-103); the server
309
+ // re-validates every entry.
310
+ var tools: [CrowdPlayAgentTool]?
311
+ if let toolsJSON = agent["toolsJSON"] as? String,
312
+ let data = toolsJSON.data(using: .utf8),
313
+ let entries = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] {
314
+ let parsed: [CrowdPlayAgentTool] = entries.compactMap { entry in
315
+ guard let name = entry["name"] as? String,
316
+ let description = entry["description"] as? String else { return nil }
317
+ var parametersJSON = "{}"
318
+ if let parameters = entry["parameters"],
319
+ JSONSerialization.isValidJSONObject(parameters),
320
+ let pData = try? JSONSerialization.data(withJSONObject: parameters),
321
+ let pString = String(data: pData, encoding: .utf8) {
322
+ parametersJSON = pString
323
+ }
324
+ return CrowdPlayAgentTool(name: name, description: description,
325
+ parametersJSON: parametersJSON)
326
+ }
327
+ if !parsed.isEmpty { tools = parsed }
328
+ }
329
+ if voice != nil || prompt != nil || context != nil || tools != nil {
330
+ agentOptions = CrowdPlayAgentOptions(voice: voice, systemPrompt: prompt,
331
+ context: context, tools: tools)
299
332
  }
300
333
  }
301
334
  Task { @MainActor in
@@ -360,6 +393,19 @@ public final class CrowdPlayRNModule: RCTEventEmitter {
360
393
  }
361
394
  }
362
395
 
396
+ @objc public func respondToAgentToolCall(_ callId: NSString, output: NSString) {
397
+ Task { @MainActor in
398
+ await CrowdPlayRNCore.shared.engine.respondToAgentToolCall(
399
+ callId: callId as String, output: output as String)
400
+ }
401
+ }
402
+
403
+ @objc public func updateAgentContext(_ text: NSString) {
404
+ Task { @MainActor in
405
+ await CrowdPlayRNCore.shared.engine.updateAgentContext(text as String)
406
+ }
407
+ }
408
+
363
409
  @objc public func retryRecording() {
364
410
  Task { @MainActor in
365
411
  let core = CrowdPlayRNCore.shared
@@ -25,6 +25,9 @@ RCT_EXTERN_METHOD(setCameraEnabled : (BOOL)enabled
25
25
  resolver : (RCTPromiseResolveBlock)resolve
26
26
  rejecter : (RCTPromiseRejectBlock)reject)
27
27
  RCT_EXTERN_METHOD(setAudioOutput : (NSString *)output)
28
+ RCT_EXTERN_METHOD(respondToAgentToolCall : (NSString *)callId
29
+ output : (NSString *)output)
30
+ RCT_EXTERN_METHOD(updateAgentContext : (NSString *)text)
28
31
  RCT_EXTERN_METHOD(retryRecording)
29
32
  RCT_EXTERN_METHOD(retryUploads)
30
33
  RCT_EXTERN_METHOD(warmUp)
package/lib/index.d.ts CHANGED
@@ -74,6 +74,17 @@ export interface ConsentGrant {
74
74
  * apply to the whole room. The app can never change the provider, the
75
75
  * model, or the API key from here.
76
76
  */
77
+ /** A tool the app offers the voice AI for one session (D-103). Requires
78
+ * the app's dashboard config to use the GPT-Live provider with an AI brain
79
+ * (delegation) enabled; ignored otherwise. */
80
+ export interface AgentTool {
81
+ /** Letters, digits, underscore, dash; max 64 chars. */
82
+ name: string;
83
+ /** What the tool does and when to use it, for the model. */
84
+ description: string;
85
+ /** JSON-schema object describing the arguments. Omit for none. */
86
+ parameters?: Record<string, unknown>;
87
+ }
77
88
  export interface AgentOptions {
78
89
  /** A voice of the app's configured provider. Gemini: Charon, Aoede,
79
90
  * Fenrir, Kore, Puck. OpenAI: marin, cedar, alloy, ash, ballad, coral,
@@ -87,6 +98,9 @@ export interface AgentOptions {
87
98
  * keep a few sentences per user in your own storage and pass them
88
99
  * here. Max 4000 chars. */
89
100
  context?: string;
101
+ /** Tools the AI may call this session (D-103). Handle calls with
102
+ * CrowdPlay.onAgentToolCall(). Max 16. */
103
+ tools?: AgentTool[];
90
104
  }
91
105
  export type Phase = 'idle' | 'connecting' | 'connected' | 'failed';
92
106
  export interface RecordingSnapshot {
@@ -167,6 +181,13 @@ export interface CrowdPlayEvents {
167
181
  detectedOutputs: string[];
168
182
  headphonesConnected: boolean;
169
183
  };
184
+ /** The voice AI asked the app to run a tool (D-103). Prefer
185
+ * CrowdPlay.onAgentToolCall(), which answers for you. */
186
+ agentToolCall: {
187
+ callId: string;
188
+ name: string;
189
+ argumentsJSON: string;
190
+ };
170
191
  }
171
192
  export type CrowdPlayEventName = keyof CrowdPlayEvents;
172
193
  declare const CrowdPlay: {
@@ -216,6 +237,19 @@ declare const CrowdPlay: {
216
237
  /** Integration self-check: configuration, permissions, disk, backend
217
238
  * reachability, app-key auth. Every failing check names its fix. */
218
239
  doctor(): Promise<DoctorCheck[]>;
240
+ /** Handle the voice AI's tool calls (D-103). The handler gets the tool
241
+ * name and its arguments (parsed JSON object) and returns the result as
242
+ * an object or JSON string; return null to decline on this device
243
+ * (another participant's device may answer; first answer wins). Ordinary
244
+ * app code, no AI here. The AI waits about 12 seconds per call. Returns
245
+ * a subscription; call .remove(). */
246
+ onAgentToolCall(handler: (name: string, args: Record<string, unknown>) => Promise<Record<string, unknown> | string | null> | Record<string, unknown> | string | null): {
247
+ remove(): void;
248
+ };
249
+ /** Tell the AI what just happened in the app, mid-session ("Night ended.
250
+ * Sam was eliminated."). Keep it to a sentence or two; long pushes are
251
+ * truncated. GPT-Live provider only; other providers ignore it. */
252
+ updateAgentContext(text: string): void;
219
253
  /** Subscribe to state events. Returns a subscription; call .remove(). */
220
254
  addListener<E extends CrowdPlayEventName>(event: E, listener: (payload: CrowdPlayEvents[E]) => void): {
221
255
  remove(): void;
package/lib/index.js CHANGED
@@ -102,6 +102,11 @@ const CrowdPlay = {
102
102
  put('voice', agent.voice, 60);
103
103
  put('systemPrompt', agent.systemPrompt, 8192);
104
104
  put('context', agent.context, 4000);
105
+ // Tools cross the bridge as one JSON string; the native layer and the
106
+ // server both re-validate (D-103).
107
+ if (Array.isArray(agent.tools) && agent.tools.length) {
108
+ put('toolsJSON', JSON.stringify(agent.tools.slice(0, 16)), 8192);
109
+ }
105
110
  }
106
111
  return native().join(displayName.trim(), roomCode, consent.grantedAtMs, agentFields);
107
112
  },
@@ -144,6 +149,37 @@ const CrowdPlay = {
144
149
  doctor() {
145
150
  return native().doctor();
146
151
  },
152
+ /** Handle the voice AI's tool calls (D-103). The handler gets the tool
153
+ * name and its arguments (parsed JSON object) and returns the result as
154
+ * an object or JSON string; return null to decline on this device
155
+ * (another participant's device may answer; first answer wins). Ordinary
156
+ * app code, no AI here. The AI waits about 12 seconds per call. Returns
157
+ * a subscription; call .remove(). */
158
+ onAgentToolCall(handler) {
159
+ const subscription = events().addListener('crowdplay:agentToolCall', (payload) => {
160
+ void (async () => {
161
+ let args = {};
162
+ try {
163
+ args = JSON.parse(payload.argumentsJSON);
164
+ }
165
+ catch { /* {} */ }
166
+ const result = await handler(payload.name, args);
167
+ if (result === null || result === undefined)
168
+ return;
169
+ const output = typeof result === 'string' ? result : JSON.stringify(result);
170
+ native().respondToAgentToolCall(payload.callId, output);
171
+ })();
172
+ });
173
+ return { remove: () => subscription.remove() };
174
+ },
175
+ /** Tell the AI what just happened in the app, mid-session ("Night ended.
176
+ * Sam was eliminated."). Keep it to a sentence or two; long pushes are
177
+ * truncated. GPT-Live provider only; other providers ignore it. */
178
+ updateAgentContext(text) {
179
+ if (typeof text === 'string' && text.trim()) {
180
+ native().updateAgentContext(text.trim());
181
+ }
182
+ },
147
183
  /** Subscribe to state events. Returns a subscription; call .remove(). */
148
184
  addListener(event, listener) {
149
185
  const subscription = events().addListener(`crowdplay:${event}`, listener);
package/llms.txt CHANGED
@@ -169,13 +169,38 @@ the voice and the persona and give a short context text for one session:
169
169
  agent: { voice: 'Aoede', context: "The user's name is Sam. Last time you practised past tense." } });
170
170
  voice must be one of the configured provider's voices (Gemini: Charon,
171
171
  Aoede, Fenrir, Kore, Puck; OpenAI: marin, cedar, alloy, ash, ballad,
172
- coral, echo, sage, shimmer, verse; Grok: Ara, Rex, Sal, Eve, Leo), unknown
172
+ coral, echo, sage, shimmer, verse; Grok: Ara, Rex, Sal, Eve, Leo;
173
+ OpenAI GPT-Live: quartz, ripple, vesper, willow, stone, gleam, meridian,
174
+ bossa, tempo, beacon, delta, cinder), unknown
173
175
  falls back to the dashboard voice. Caps: systemPrompt 8192 chars, context
174
176
  4000. The options of the join that CREATES the AI apply to the whole room;
175
177
  fixed for the session (leave and join again to change); provider, model
176
178
  and key cannot be changed from the app. MEMORY is built with `context`:
177
179
  keep a few sentences per user in the app's own storage and pass them at
178
180
  every join.
181
+ Agent tools + live context (0.5.0+, D-103; needs the app's dashboard
182
+ config on the OpenAI GPT-Live provider with an AI brain enabled —
183
+ ignored otherwise, never an error). Tools let the AI SEE app state and
184
+ ACT in the app: pass them at join, handle calls with ordinary JS.
185
+ await CrowdPlay.join({ displayName, roomCode, consent: grant,
186
+ agent: { tools: [
187
+ { name: 'get_game_state',
188
+ description: 'The current players, roles you know, and votes.' },
189
+ { name: 'cast_vote', description: 'Vote to eliminate one player.',
190
+ parameters: { type: 'object',
191
+ properties: { player: { type: 'string' } }, required: ['player'] } },
192
+ ] } });
193
+ const sub = CrowdPlay.onAgentToolCall(async (name, args) => {
194
+ if (name === 'get_game_state') return game.state();
195
+ if (name === 'cast_vote') return game.castVote(args.player);
196
+ return null; // decline on this device; first device to answer wins
197
+ });
198
+ Return an object (or JSON string), or null to decline. The AI waits
199
+ about 12 seconds per call, then carries on gracefully. Max 16 tools;
200
+ names letters/digits/underscore/dash up to 64 chars. To push events to
201
+ the AI as they happen (it has no other way to know):
202
+ CrowdPlay.updateAgentContext('Night ended. Sam was eliminated.') — a
203
+ sentence or two, long pushes are truncated.
179
204
  Conversations are recorded and transcribed by the platform automatically.
180
205
  RECOMMENDED: voice-AI apps should be AUDIO-ONLY (audioOnly: true; no
181
206
  camera permission, no video anywhere); a camera adds nothing to talking
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crowdplaysdk",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "description": "CrowdPlay lossless conversation capture for React Native (iOS). Studio-grade per-participant recording during live video calls, delivered to CrowdPlay automatically.",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
package/src/index.ts CHANGED
@@ -84,6 +84,18 @@ export interface ConsentGrant {
84
84
  * apply to the whole room. The app can never change the provider, the
85
85
  * model, or the API key from here.
86
86
  */
87
+ /** A tool the app offers the voice AI for one session (D-103). Requires
88
+ * the app's dashboard config to use the GPT-Live provider with an AI brain
89
+ * (delegation) enabled; ignored otherwise. */
90
+ export interface AgentTool {
91
+ /** Letters, digits, underscore, dash; max 64 chars. */
92
+ name: string;
93
+ /** What the tool does and when to use it, for the model. */
94
+ description: string;
95
+ /** JSON-schema object describing the arguments. Omit for none. */
96
+ parameters?: Record<string, unknown>;
97
+ }
98
+
87
99
  export interface AgentOptions {
88
100
  /** A voice of the app's configured provider. Gemini: Charon, Aoede,
89
101
  * Fenrir, Kore, Puck. OpenAI: marin, cedar, alloy, ash, ballad, coral,
@@ -97,6 +109,9 @@ export interface AgentOptions {
97
109
  * keep a few sentences per user in your own storage and pass them
98
110
  * here. Max 4000 chars. */
99
111
  context?: string;
112
+ /** Tools the AI may call this session (D-103). Handle calls with
113
+ * CrowdPlay.onAgentToolCall(). Max 16. */
114
+ tools?: AgentTool[];
100
115
  }
101
116
 
102
117
  export type Phase = 'idle' | 'connecting' | 'connected' | 'failed';
@@ -164,6 +179,9 @@ export interface CrowdPlayEvents {
164
179
  * "iPhone speaker"), connected candidates, and whether headphones are
165
180
  * attached. Drive the call screen's output menu from this (D-079). */
166
181
  audioRoute: { currentOutputName: string; detectedOutputs: string[]; headphonesConnected: boolean };
182
+ /** The voice AI asked the app to run a tool (D-103). Prefer
183
+ * CrowdPlay.onAgentToolCall(), which answers for you. */
184
+ agentToolCall: { callId: string; name: string; argumentsJSON: string };
167
185
  }
168
186
 
169
187
  export type CrowdPlayEventName = keyof CrowdPlayEvents;
@@ -177,6 +195,8 @@ interface NativeCrowdPlay {
177
195
  consentText(): Promise<string>;
178
196
  join(displayName: string, roomCode: string, consentGrantedAtMs: number,
179
197
  agent: Record<string, string> | null): Promise<void>;
198
+ respondToAgentToolCall(callId: string, output: string): void;
199
+ updateAgentContext(text: string): void;
180
200
  leave(): Promise<void>;
181
201
  setMicMuted(muted: boolean): Promise<void>;
182
202
  setCameraEnabled(enabled: boolean): Promise<void>;
@@ -279,6 +299,11 @@ const CrowdPlay = {
279
299
  put('voice', agent.voice, 60);
280
300
  put('systemPrompt', agent.systemPrompt, 8192);
281
301
  put('context', agent.context, 4000);
302
+ // Tools cross the bridge as one JSON string; the native layer and the
303
+ // server both re-validate (D-103).
304
+ if (Array.isArray(agent.tools) && agent.tools.length) {
305
+ put('toolsJSON', JSON.stringify(agent.tools.slice(0, 16)), 8192);
306
+ }
282
307
  }
283
308
  return native().join(displayName.trim(), roomCode, consent.grantedAtMs, agentFields);
284
309
  },
@@ -330,6 +355,41 @@ const CrowdPlay = {
330
355
  return native().doctor();
331
356
  },
332
357
 
358
+ /** Handle the voice AI's tool calls (D-103). The handler gets the tool
359
+ * name and its arguments (parsed JSON object) and returns the result as
360
+ * an object or JSON string; return null to decline on this device
361
+ * (another participant's device may answer; first answer wins). Ordinary
362
+ * app code, no AI here. The AI waits about 12 seconds per call. Returns
363
+ * a subscription; call .remove(). */
364
+ onAgentToolCall(
365
+ handler: (name: string, args: Record<string, unknown>) =>
366
+ Promise<Record<string, unknown> | string | null> | Record<string, unknown> | string | null
367
+ ): { remove(): void } {
368
+ const subscription = events().addListener(
369
+ 'crowdplay:agentToolCall',
370
+ (payload: { callId: string; name: string; argumentsJSON: string }) => {
371
+ void (async () => {
372
+ let args: Record<string, unknown> = {};
373
+ try { args = JSON.parse(payload.argumentsJSON); } catch { /* {} */ }
374
+ const result = await handler(payload.name, args);
375
+ if (result === null || result === undefined) return;
376
+ const output = typeof result === 'string' ? result : JSON.stringify(result);
377
+ native().respondToAgentToolCall(payload.callId, output);
378
+ })();
379
+ }
380
+ );
381
+ return { remove: () => subscription.remove() };
382
+ },
383
+
384
+ /** Tell the AI what just happened in the app, mid-session ("Night ended.
385
+ * Sam was eliminated."). Keep it to a sentence or two; long pushes are
386
+ * truncated. GPT-Live provider only; other providers ignore it. */
387
+ updateAgentContext(text: string): void {
388
+ if (typeof text === 'string' && text.trim()) {
389
+ native().updateAgentContext(text.trim());
390
+ }
391
+ },
392
+
333
393
  /** Subscribe to state events. Returns a subscription; call .remove(). */
334
394
  addListener<E extends CrowdPlayEventName>(
335
395
  event: E,