@voicethere/agent 0.8.0 → 0.8.2

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.
@@ -10915,6 +10915,12 @@ function isRecordingControlAckMessage(value) {
10915
10915
  const msg = value;
10916
10916
  return msg.type === "recording_control_ack" && typeof msg.requestId === "string";
10917
10917
  }
10918
+ function isConversationHistoryControlAckMessage(value) {
10919
+ if (!value || typeof value !== "object")
10920
+ return false;
10921
+ const msg = value;
10922
+ return msg.type === "conversation_history_control_ack" && typeof msg.requestId === "string";
10923
+ }
10918
10924
  function isMixControlAckMessage(value) {
10919
10925
  if (!value || typeof value !== "object")
10920
10926
  return false;
@@ -10969,7 +10975,7 @@ function isParentMessage(value) {
10969
10975
  if (!value || typeof value !== "object")
10970
10976
  return false;
10971
10977
  const msg = value;
10972
- return msg.type === "session_start" || msg.type === "speech_event" || msg.type === "session_end" || msg.type === "data_channel_message" || msg.type === "data_channel_binary" || msg.type === "idle_timeout" || msg.type === "recording_control_ack" || msg.type === "play_ack" || msg.type === "play_status_ack" || msg.type === "play_stop_ack" || msg.type === "play_pose_ack" || msg.type === "mix_control_ack" || msg.type === "stt_control_ack" || msg.type === "webhook";
10978
+ return msg.type === "session_start" || msg.type === "speech_event" || msg.type === "session_end" || msg.type === "data_channel_message" || msg.type === "data_channel_binary" || msg.type === "idle_timeout" || msg.type === "recording_control_ack" || msg.type === "conversation_history_control_ack" || msg.type === "play_ack" || msg.type === "play_status_ack" || msg.type === "play_stop_ack" || msg.type === "play_pose_ack" || msg.type === "mix_control_ack" || msg.type === "stt_control_ack" || msg.type === "webhook";
10973
10979
  }
10974
10980
  function isSessionScopedParentMessage(value) {
10975
10981
  return isParentMessage(value) && !isWebhookMessage(value);
@@ -10983,11 +10989,13 @@ function parseDataChannelPayload(raw) {
10983
10989
  }
10984
10990
  var peerEnvBySessionId = /* @__PURE__ */ new Map();
10985
10991
  var recordingAvailableBySessionId = /* @__PURE__ */ new Map();
10992
+ var conversationHistoryAvailableBySessionId = /* @__PURE__ */ new Map();
10986
10993
  var mixAvailableBySessionId = /* @__PURE__ */ new Map();
10987
10994
  var ttsPoseAvailableBySessionId = /* @__PURE__ */ new Map();
10988
10995
  var endedSessionIds = /* @__PURE__ */ new Set();
10989
10996
  var PLAY_BYTES_MAX_DECODED_LENGTH = 64 * 1024;
10990
10997
  var pendingRecordingAcks = /* @__PURE__ */ new Map();
10998
+ var pendingConversationHistoryAcks = /* @__PURE__ */ new Map();
10991
10999
  var pendingPlayAcks = /* @__PURE__ */ new Map();
10992
11000
  var pendingPlayStatusAcks = /* @__PURE__ */ new Map();
10993
11001
  var pendingPlayStopAcks = /* @__PURE__ */ new Map();
@@ -11006,6 +11014,18 @@ function handleRecordingControlAck(message) {
11006
11014
  requestId: message.requestId
11007
11015
  });
11008
11016
  }
11017
+ function handleConversationHistoryControlAck(message) {
11018
+ const pending = pendingConversationHistoryAcks.get(message.requestId);
11019
+ if (!pending)
11020
+ return;
11021
+ clearTimeout(pending.timer);
11022
+ pendingConversationHistoryAcks.delete(message.requestId);
11023
+ pending.resolve({
11024
+ ok: message.ok,
11025
+ reason: message.reason,
11026
+ requestId: message.requestId
11027
+ });
11028
+ }
11009
11029
  function handlePlayAck(message) {
11010
11030
  const pending = pendingPlayAcks.get(message.requestId);
11011
11031
  if (!pending)
@@ -11093,6 +11113,15 @@ function clearPendingRecordingAcksForSession(sessionId, reason) {
11093
11113
  pending.resolve({ ok: false, reason, requestId });
11094
11114
  }
11095
11115
  }
11116
+ function clearPendingConversationHistoryAcksForSession(sessionId, reason) {
11117
+ for (const [requestId, pending] of pendingConversationHistoryAcks) {
11118
+ if (pending.sessionId !== sessionId)
11119
+ continue;
11120
+ clearTimeout(pending.timer);
11121
+ pendingConversationHistoryAcks.delete(requestId);
11122
+ pending.resolve({ ok: false, reason, requestId });
11123
+ }
11124
+ }
11096
11125
  var sessionExecutionContext = new AsyncLocalStorage();
11097
11126
  var agentLogSessionContext = new AsyncLocalStorage();
11098
11127
  var inboundQueueAuthority = null;
@@ -11239,6 +11268,7 @@ async function handleParentMessage(message, handlers) {
11239
11268
  endedSessionIds.delete(message.sessionId);
11240
11269
  peerEnvBySessionId.set(message.sessionId, message.env);
11241
11270
  recordingAvailableBySessionId.set(message.sessionId, message.recordingAvailable ?? false);
11271
+ conversationHistoryAvailableBySessionId.set(message.sessionId, message.conversationHistoryAvailable ?? false);
11242
11272
  mixAvailableBySessionId.set(message.sessionId, message.mixAvailable ?? false);
11243
11273
  ttsPoseAvailableBySessionId.set(message.sessionId, message.ttsPoseAvailable ?? false);
11244
11274
  const sessionStartInitDelayMs = resolveSessionStartInitDelayMs();
@@ -11249,6 +11279,7 @@ async function handleParentMessage(message, handlers) {
11249
11279
  sessionId: message.sessionId,
11250
11280
  env: message.env,
11251
11281
  recordingAvailable: message.recordingAvailable ?? false,
11282
+ conversationHistoryAvailable: message.conversationHistoryAvailable ?? false,
11252
11283
  mixAvailable: message.mixAvailable ?? false,
11253
11284
  ttsPoseAvailable: message.ttsPoseAvailable ?? false
11254
11285
  });
@@ -11295,8 +11326,10 @@ async function handleParentMessage(message, handlers) {
11295
11326
  break;
11296
11327
  case "session_end":
11297
11328
  clearPendingRecordingAcksForSession(message.sessionId, "session_ended");
11329
+ clearPendingConversationHistoryAcksForSession(message.sessionId, "session_ended");
11298
11330
  peerEnvBySessionId.delete(message.sessionId);
11299
11331
  recordingAvailableBySessionId.delete(message.sessionId);
11332
+ conversationHistoryAvailableBySessionId.delete(message.sessionId);
11300
11333
  mixAvailableBySessionId.delete(message.sessionId);
11301
11334
  ttsPoseAvailableBySessionId.delete(message.sessionId);
11302
11335
  await (handlers.onClientLeave ?? handlers.onSessionEnd)?.({
@@ -11318,6 +11351,10 @@ function defineAgent(handlers) {
11318
11351
  handleRecordingControlAck(message);
11319
11352
  return;
11320
11353
  }
11354
+ if (isConversationHistoryControlAckMessage(message)) {
11355
+ handleConversationHistoryControlAck(message);
11356
+ return;
11357
+ }
11321
11358
  if (isPlayAckMessage(message)) {
11322
11359
  handlePlayAck(message);
11323
11360
  return;
@@ -11351,6 +11388,7 @@ function defineAgent(handlers) {
11351
11388
  if (message.type === "session_end") {
11352
11389
  endedSessionIds.add(message.sessionId);
11353
11390
  clearPendingRecordingAcksForSession(message.sessionId, "session_ended");
11391
+ clearPendingConversationHistoryAcksForSession(message.sessionId, "session_ended");
11354
11392
  inboundBySession.clear(message.sessionId);
11355
11393
  }
11356
11394
  if (message.type === "session_start") {
@@ -160,6 +160,12 @@ function isRecordingControlAckMessage(value) {
160
160
  const msg = value;
161
161
  return msg.type === "recording_control_ack" && typeof msg.requestId === "string";
162
162
  }
163
+ function isConversationHistoryControlAckMessage(value) {
164
+ if (!value || typeof value !== "object")
165
+ return false;
166
+ const msg = value;
167
+ return msg.type === "conversation_history_control_ack" && typeof msg.requestId === "string";
168
+ }
163
169
  function isMixControlAckMessage(value) {
164
170
  if (!value || typeof value !== "object")
165
171
  return false;
@@ -214,7 +220,7 @@ function isParentMessage(value) {
214
220
  if (!value || typeof value !== "object")
215
221
  return false;
216
222
  const msg = value;
217
- return msg.type === "session_start" || msg.type === "speech_event" || msg.type === "session_end" || msg.type === "data_channel_message" || msg.type === "data_channel_binary" || msg.type === "idle_timeout" || msg.type === "recording_control_ack" || msg.type === "play_ack" || msg.type === "play_status_ack" || msg.type === "play_stop_ack" || msg.type === "play_pose_ack" || msg.type === "mix_control_ack" || msg.type === "stt_control_ack" || msg.type === "webhook";
223
+ return msg.type === "session_start" || msg.type === "speech_event" || msg.type === "session_end" || msg.type === "data_channel_message" || msg.type === "data_channel_binary" || msg.type === "idle_timeout" || msg.type === "recording_control_ack" || msg.type === "conversation_history_control_ack" || msg.type === "play_ack" || msg.type === "play_status_ack" || msg.type === "play_stop_ack" || msg.type === "play_pose_ack" || msg.type === "mix_control_ack" || msg.type === "stt_control_ack" || msg.type === "webhook";
218
224
  }
219
225
  function isSessionScopedParentMessage(value) {
220
226
  return isParentMessage(value) && !isWebhookMessage(value);
@@ -228,11 +234,13 @@ function parseDataChannelPayload(raw) {
228
234
  }
229
235
  var peerEnvBySessionId = /* @__PURE__ */ new Map();
230
236
  var recordingAvailableBySessionId = /* @__PURE__ */ new Map();
237
+ var conversationHistoryAvailableBySessionId = /* @__PURE__ */ new Map();
231
238
  var mixAvailableBySessionId = /* @__PURE__ */ new Map();
232
239
  var ttsPoseAvailableBySessionId = /* @__PURE__ */ new Map();
233
240
  var endedSessionIds = /* @__PURE__ */ new Set();
234
241
  var PLAY_BYTES_MAX_DECODED_LENGTH = 64 * 1024;
235
242
  var pendingRecordingAcks = /* @__PURE__ */ new Map();
243
+ var pendingConversationHistoryAcks = /* @__PURE__ */ new Map();
236
244
  var pendingPlayAcks = /* @__PURE__ */ new Map();
237
245
  var pendingPlayStatusAcks = /* @__PURE__ */ new Map();
238
246
  var pendingPlayStopAcks = /* @__PURE__ */ new Map();
@@ -251,6 +259,18 @@ function handleRecordingControlAck(message) {
251
259
  requestId: message.requestId
252
260
  });
253
261
  }
262
+ function handleConversationHistoryControlAck(message) {
263
+ const pending = pendingConversationHistoryAcks.get(message.requestId);
264
+ if (!pending)
265
+ return;
266
+ clearTimeout(pending.timer);
267
+ pendingConversationHistoryAcks.delete(message.requestId);
268
+ pending.resolve({
269
+ ok: message.ok,
270
+ reason: message.reason,
271
+ requestId: message.requestId
272
+ });
273
+ }
254
274
  function handlePlayAck(message) {
255
275
  const pending = pendingPlayAcks.get(message.requestId);
256
276
  if (!pending)
@@ -338,6 +358,15 @@ function clearPendingRecordingAcksForSession(sessionId, reason) {
338
358
  pending.resolve({ ok: false, reason, requestId });
339
359
  }
340
360
  }
361
+ function clearPendingConversationHistoryAcksForSession(sessionId, reason) {
362
+ for (const [requestId, pending] of pendingConversationHistoryAcks) {
363
+ if (pending.sessionId !== sessionId)
364
+ continue;
365
+ clearTimeout(pending.timer);
366
+ pendingConversationHistoryAcks.delete(requestId);
367
+ pending.resolve({ ok: false, reason, requestId });
368
+ }
369
+ }
341
370
  var sessionExecutionContext = new AsyncLocalStorage();
342
371
  var agentLogSessionContext = new AsyncLocalStorage();
343
372
  var inboundQueueAuthority = null;
@@ -484,6 +513,7 @@ async function handleParentMessage(message, handlers) {
484
513
  endedSessionIds.delete(message.sessionId);
485
514
  peerEnvBySessionId.set(message.sessionId, message.env);
486
515
  recordingAvailableBySessionId.set(message.sessionId, message.recordingAvailable ?? false);
516
+ conversationHistoryAvailableBySessionId.set(message.sessionId, message.conversationHistoryAvailable ?? false);
487
517
  mixAvailableBySessionId.set(message.sessionId, message.mixAvailable ?? false);
488
518
  ttsPoseAvailableBySessionId.set(message.sessionId, message.ttsPoseAvailable ?? false);
489
519
  const sessionStartInitDelayMs = resolveSessionStartInitDelayMs();
@@ -494,6 +524,7 @@ async function handleParentMessage(message, handlers) {
494
524
  sessionId: message.sessionId,
495
525
  env: message.env,
496
526
  recordingAvailable: message.recordingAvailable ?? false,
527
+ conversationHistoryAvailable: message.conversationHistoryAvailable ?? false,
497
528
  mixAvailable: message.mixAvailable ?? false,
498
529
  ttsPoseAvailable: message.ttsPoseAvailable ?? false
499
530
  });
@@ -540,8 +571,10 @@ async function handleParentMessage(message, handlers) {
540
571
  break;
541
572
  case "session_end":
542
573
  clearPendingRecordingAcksForSession(message.sessionId, "session_ended");
574
+ clearPendingConversationHistoryAcksForSession(message.sessionId, "session_ended");
543
575
  peerEnvBySessionId.delete(message.sessionId);
544
576
  recordingAvailableBySessionId.delete(message.sessionId);
577
+ conversationHistoryAvailableBySessionId.delete(message.sessionId);
545
578
  mixAvailableBySessionId.delete(message.sessionId);
546
579
  ttsPoseAvailableBySessionId.delete(message.sessionId);
547
580
  await (handlers.onClientLeave ?? handlers.onSessionEnd)?.({
@@ -563,6 +596,10 @@ function defineAgent(handlers) {
563
596
  handleRecordingControlAck(message);
564
597
  return;
565
598
  }
599
+ if (isConversationHistoryControlAckMessage(message)) {
600
+ handleConversationHistoryControlAck(message);
601
+ return;
602
+ }
566
603
  if (isPlayAckMessage(message)) {
567
604
  handlePlayAck(message);
568
605
  return;
@@ -596,6 +633,7 @@ function defineAgent(handlers) {
596
633
  if (message.type === "session_end") {
597
634
  endedSessionIds.add(message.sessionId);
598
635
  clearPendingRecordingAcksForSession(message.sessionId, "session_ended");
636
+ clearPendingConversationHistoryAcksForSession(message.sessionId, "session_ended");
599
637
  inboundBySession.clear(message.sessionId);
600
638
  }
601
639
  if (message.type === "session_start") {
@@ -160,6 +160,12 @@ function isRecordingControlAckMessage(value) {
160
160
  const msg = value;
161
161
  return msg.type === "recording_control_ack" && typeof msg.requestId === "string";
162
162
  }
163
+ function isConversationHistoryControlAckMessage(value) {
164
+ if (!value || typeof value !== "object")
165
+ return false;
166
+ const msg = value;
167
+ return msg.type === "conversation_history_control_ack" && typeof msg.requestId === "string";
168
+ }
163
169
  function isMixControlAckMessage(value) {
164
170
  if (!value || typeof value !== "object")
165
171
  return false;
@@ -214,7 +220,7 @@ function isParentMessage(value) {
214
220
  if (!value || typeof value !== "object")
215
221
  return false;
216
222
  const msg = value;
217
- return msg.type === "session_start" || msg.type === "speech_event" || msg.type === "session_end" || msg.type === "data_channel_message" || msg.type === "data_channel_binary" || msg.type === "idle_timeout" || msg.type === "recording_control_ack" || msg.type === "play_ack" || msg.type === "play_status_ack" || msg.type === "play_stop_ack" || msg.type === "play_pose_ack" || msg.type === "mix_control_ack" || msg.type === "stt_control_ack" || msg.type === "webhook";
223
+ return msg.type === "session_start" || msg.type === "speech_event" || msg.type === "session_end" || msg.type === "data_channel_message" || msg.type === "data_channel_binary" || msg.type === "idle_timeout" || msg.type === "recording_control_ack" || msg.type === "conversation_history_control_ack" || msg.type === "play_ack" || msg.type === "play_status_ack" || msg.type === "play_stop_ack" || msg.type === "play_pose_ack" || msg.type === "mix_control_ack" || msg.type === "stt_control_ack" || msg.type === "webhook";
218
224
  }
219
225
  function isSessionScopedParentMessage(value) {
220
226
  return isParentMessage(value) && !isWebhookMessage(value);
@@ -228,11 +234,13 @@ function parseDataChannelPayload(raw) {
228
234
  }
229
235
  var peerEnvBySessionId = /* @__PURE__ */ new Map();
230
236
  var recordingAvailableBySessionId = /* @__PURE__ */ new Map();
237
+ var conversationHistoryAvailableBySessionId = /* @__PURE__ */ new Map();
231
238
  var mixAvailableBySessionId = /* @__PURE__ */ new Map();
232
239
  var ttsPoseAvailableBySessionId = /* @__PURE__ */ new Map();
233
240
  var endedSessionIds = /* @__PURE__ */ new Set();
234
241
  var PLAY_BYTES_MAX_DECODED_LENGTH = 64 * 1024;
235
242
  var pendingRecordingAcks = /* @__PURE__ */ new Map();
243
+ var pendingConversationHistoryAcks = /* @__PURE__ */ new Map();
236
244
  var pendingPlayAcks = /* @__PURE__ */ new Map();
237
245
  var pendingPlayStatusAcks = /* @__PURE__ */ new Map();
238
246
  var pendingPlayStopAcks = /* @__PURE__ */ new Map();
@@ -251,6 +259,18 @@ function handleRecordingControlAck(message) {
251
259
  requestId: message.requestId
252
260
  });
253
261
  }
262
+ function handleConversationHistoryControlAck(message) {
263
+ const pending = pendingConversationHistoryAcks.get(message.requestId);
264
+ if (!pending)
265
+ return;
266
+ clearTimeout(pending.timer);
267
+ pendingConversationHistoryAcks.delete(message.requestId);
268
+ pending.resolve({
269
+ ok: message.ok,
270
+ reason: message.reason,
271
+ requestId: message.requestId
272
+ });
273
+ }
254
274
  function handlePlayAck(message) {
255
275
  const pending = pendingPlayAcks.get(message.requestId);
256
276
  if (!pending)
@@ -338,6 +358,15 @@ function clearPendingRecordingAcksForSession(sessionId, reason) {
338
358
  pending.resolve({ ok: false, reason, requestId });
339
359
  }
340
360
  }
361
+ function clearPendingConversationHistoryAcksForSession(sessionId, reason) {
362
+ for (const [requestId, pending] of pendingConversationHistoryAcks) {
363
+ if (pending.sessionId !== sessionId)
364
+ continue;
365
+ clearTimeout(pending.timer);
366
+ pendingConversationHistoryAcks.delete(requestId);
367
+ pending.resolve({ ok: false, reason, requestId });
368
+ }
369
+ }
341
370
  var sessionExecutionContext = new AsyncLocalStorage();
342
371
  var agentLogSessionContext = new AsyncLocalStorage();
343
372
  var inboundQueueAuthority = null;
@@ -484,6 +513,7 @@ async function handleParentMessage(message, handlers) {
484
513
  endedSessionIds.delete(message.sessionId);
485
514
  peerEnvBySessionId.set(message.sessionId, message.env);
486
515
  recordingAvailableBySessionId.set(message.sessionId, message.recordingAvailable ?? false);
516
+ conversationHistoryAvailableBySessionId.set(message.sessionId, message.conversationHistoryAvailable ?? false);
487
517
  mixAvailableBySessionId.set(message.sessionId, message.mixAvailable ?? false);
488
518
  ttsPoseAvailableBySessionId.set(message.sessionId, message.ttsPoseAvailable ?? false);
489
519
  const sessionStartInitDelayMs = resolveSessionStartInitDelayMs();
@@ -494,6 +524,7 @@ async function handleParentMessage(message, handlers) {
494
524
  sessionId: message.sessionId,
495
525
  env: message.env,
496
526
  recordingAvailable: message.recordingAvailable ?? false,
527
+ conversationHistoryAvailable: message.conversationHistoryAvailable ?? false,
497
528
  mixAvailable: message.mixAvailable ?? false,
498
529
  ttsPoseAvailable: message.ttsPoseAvailable ?? false
499
530
  });
@@ -540,8 +571,10 @@ async function handleParentMessage(message, handlers) {
540
571
  break;
541
572
  case "session_end":
542
573
  clearPendingRecordingAcksForSession(message.sessionId, "session_ended");
574
+ clearPendingConversationHistoryAcksForSession(message.sessionId, "session_ended");
543
575
  peerEnvBySessionId.delete(message.sessionId);
544
576
  recordingAvailableBySessionId.delete(message.sessionId);
577
+ conversationHistoryAvailableBySessionId.delete(message.sessionId);
545
578
  mixAvailableBySessionId.delete(message.sessionId);
546
579
  ttsPoseAvailableBySessionId.delete(message.sessionId);
547
580
  await (handlers.onClientLeave ?? handlers.onSessionEnd)?.({
@@ -563,6 +596,10 @@ function defineAgent(handlers) {
563
596
  handleRecordingControlAck(message);
564
597
  return;
565
598
  }
599
+ if (isConversationHistoryControlAckMessage(message)) {
600
+ handleConversationHistoryControlAck(message);
601
+ return;
602
+ }
566
603
  if (isPlayAckMessage(message)) {
567
604
  handlePlayAck(message);
568
605
  return;
@@ -596,6 +633,7 @@ function defineAgent(handlers) {
596
633
  if (message.type === "session_end") {
597
634
  endedSessionIds.add(message.sessionId);
598
635
  clearPendingRecordingAcksForSession(message.sessionId, "session_ended");
636
+ clearPendingConversationHistoryAcksForSession(message.sessionId, "session_ended");
599
637
  inboundBySession.clear(message.sessionId);
600
638
  }
601
639
  if (message.type === "session_start") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voicethere/agent",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "VoiceThere customer agent SDK — IPC types and runtime helpers for sandboxed child bundles",
5
5
  "type": "module",
6
6
  "exports": {
@@ -10,7 +10,7 @@ import {
10
10
  defineAgent,
11
11
  parseChatText,
12
12
  sendToClient,
13
- speak,
13
+ speakAndChat,
14
14
  type SpeechEvent,
15
15
  } from "@voicethere/agent";
16
16
 
@@ -42,16 +42,14 @@ defineAgent({
42
42
 
43
43
  onUserSpeechFinal({ sessionId, text }) {
44
44
  const reply = formatEcho(text);
45
- speak(sessionId, reply);
46
- sendToClient(sessionId, { type: "chat_reply", text: reply });
45
+ speakAndChat(sessionId, reply, { stream: true });
47
46
  },
48
47
 
49
48
  onDataChannelMessage(ctx) {
50
49
  const text = parseChatText(ctx.message);
51
50
  if (!text) return;
52
51
  const reply = formatEcho(text);
53
- sendToClient(ctx.sessionId, { type: "chat_reply", text: reply });
54
- speak(ctx.sessionId, reply);
52
+ speakAndChat(ctx.sessionId, reply, { stream: true });
55
53
  },
56
54
 
57
55
  onSessionEnd({ sessionId }) {
@@ -10,6 +10,7 @@ import {
10
10
  parseChatText,
11
11
  sendToClient,
12
12
  speak,
13
+ speakAndChat,
13
14
  type SpeechEvent,
14
15
  } from "@voicethere/agent";
15
16
 
@@ -54,7 +55,7 @@ function deliverSpokenThenPlay(
54
55
  applyOutboundOps(
55
56
  sessionId,
56
57
  spokenThenPlayOps(result.messages, result.speakLines),
57
- { sendToClient, speak },
58
+ { sendToClient, speak, speakAndChat },
58
59
  );
59
60
  }
60
61
 
@@ -88,6 +89,7 @@ defineAgent({
88
89
  applyOutboundOps(sessionId, greetingOps(sessionId, GREETING), {
89
90
  sendToClient,
90
91
  speak,
92
+ speakAndChat,
91
93
  });
92
94
  agentLog("info", `voice-showcase session_start ${sessionId}`);
93
95
  },
@@ -3,6 +3,8 @@
3
3
  * Tests import this module directly — no defineAgent dependency.
4
4
  */
5
5
 
6
+ import { randomUUID } from "node:crypto";
7
+
6
8
  import { formatRecipeSpeech, pickRecipe } from "./recipes.js";
7
9
  import { pickFunFact } from "./fun-facts.js";
8
10
  import {
@@ -55,6 +57,8 @@ export interface ConversationState {
55
57
  export interface OutboundMessage {
56
58
  type: "chat_reply" | "menu" | "agent_event";
57
59
  text?: string;
60
+ stream?: boolean;
61
+ utteranceId?: string;
58
62
  event?: string;
59
63
  items?: Array<{ id: number; label: string }>;
60
64
  sessionId?: string;
@@ -91,9 +95,10 @@ function speakAndChat(text: string): {
91
95
  speakLines: string[];
92
96
  messages: OutboundMessage[];
93
97
  } {
98
+ const utteranceId = randomUUID();
94
99
  return {
95
100
  speakLines: [text],
96
- messages: [{ type: "chat_reply", text }],
101
+ messages: [{ type: "chat_reply", text, stream: true, utteranceId }],
97
102
  };
98
103
  }
99
104
 
@@ -427,16 +432,15 @@ export function handleUtterance(
427
432
  }
428
433
 
429
434
  if (!country) {
435
+ const ask = speakAndChat("Got it. Which country is that in?");
430
436
  return {
431
437
  state: {
432
438
  ...state,
433
439
  phase: "weatherAwaitingLocation",
434
440
  weatherCity: city,
435
441
  },
436
- speakLines: ["Got it. Which country is that in?"],
437
- messages: [
438
- { type: "chat_reply", text: "Got it. Which country is that in?" },
439
- ],
442
+ speakLines: ask.speakLines,
443
+ messages: ask.messages,
440
444
  };
441
445
  }
442
446
 
@@ -3,15 +3,21 @@
3
3
  * then trigger TTS play. Parent IPC preserves this order.
4
4
  */
5
5
 
6
+ import { randomUUID } from "node:crypto";
7
+
6
8
  import type { OutboundMessage } from "./conversation.js";
7
9
 
8
10
  export type OutboundOp =
9
- | { kind: "send"; message: OutboundMessage }
10
- | { kind: "play"; text: string };
11
+ { kind: "send"; message: OutboundMessage } | { kind: "play"; text: string };
11
12
 
12
13
  export interface OutboundDeps {
13
14
  sendToClient: (sessionId: string, payload: unknown) => void;
14
15
  speak: (sessionId: string, text: string) => void;
16
+ speakAndChat: (
17
+ sessionId: string,
18
+ text: string,
19
+ options?: { stream?: boolean; utteranceId?: string },
20
+ ) => void;
15
21
  }
16
22
 
17
23
  /** All DataChannel payloads, then TTS play commands. */
@@ -29,14 +35,17 @@ export function spokenThenPlayOps(
29
35
  return ops;
30
36
  }
31
37
 
32
- export function greetingOps(
33
- sessionId: string,
34
- greeting: string,
35
- ): OutboundOp[] {
38
+ export function greetingOps(sessionId: string, greeting: string): OutboundOp[] {
39
+ const utteranceId = randomUUID();
36
40
  return spokenThenPlayOps(
37
41
  [
38
42
  { type: "agent_event", event: "session_start", sessionId },
39
- { type: "chat_reply", text: greeting },
43
+ {
44
+ type: "chat_reply",
45
+ text: greeting,
46
+ stream: true,
47
+ utteranceId,
48
+ },
40
49
  ],
41
50
  [greeting],
42
51
  );
@@ -47,11 +56,32 @@ export function applyOutboundOps(
47
56
  ops: OutboundOp[],
48
57
  deps: OutboundDeps,
49
58
  ): void {
59
+ const spokenViaSpeakAndChat = new Set<string>();
60
+
50
61
  for (const op of ops) {
51
62
  if (op.kind === "send") {
52
- deps.sendToClient(sessionId, op.message);
53
- } else {
54
- deps.speak(sessionId, op.text);
63
+ const message = op.message;
64
+ if (
65
+ message.type === "chat_reply" &&
66
+ message.stream === true &&
67
+ message.text?.trim()
68
+ ) {
69
+ const text = message.text.trim();
70
+ deps.speakAndChat(sessionId, text, {
71
+ stream: true,
72
+ utteranceId: message.utteranceId,
73
+ });
74
+ spokenViaSpeakAndChat.add(text);
75
+ } else {
76
+ deps.sendToClient(sessionId, message);
77
+ }
78
+ continue;
79
+ }
80
+
81
+ const playText = op.text.trim();
82
+ if (spokenViaSpeakAndChat.has(playText)) {
83
+ continue;
55
84
  }
85
+ deps.speak(sessionId, op.text);
56
86
  }
57
87
  }