gemi 0.131.0 → 0.133.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.
Files changed (230) hide show
  1. package/dist/ai/AgentController.d.ts +73 -0
  2. package/dist/ai/AgentController.d.ts.map +1 -1
  3. package/dist/ai/AgentProvider.d.ts +25 -0
  4. package/dist/ai/AgentProvider.d.ts.map +1 -1
  5. package/dist/ai/VoiceBridge.d.ts +365 -0
  6. package/dist/ai/VoiceBridge.d.ts.map +1 -0
  7. package/dist/ai/index.d.ts +13 -0
  8. package/dist/ai/index.d.ts.map +1 -1
  9. package/dist/ai/index.js +18 -18
  10. package/dist/ai/index.js.map +11 -6
  11. package/dist/ai/providers/endpoints.d.ts +19 -0
  12. package/dist/ai/providers/endpoints.d.ts.map +1 -1
  13. package/dist/ai/providers/realtime.d.ts +272 -0
  14. package/dist/ai/providers/realtime.d.ts.map +1 -0
  15. package/dist/ai/providers/sideband.d.ts +254 -0
  16. package/dist/ai/providers/sideband.d.ts.map +1 -0
  17. package/dist/ai/runtime.d.ts.map +1 -1
  18. package/dist/ai/types.d.ts +15 -0
  19. package/dist/ai/types.d.ts.map +1 -1
  20. package/dist/ai/voice/index.d.ts +23 -0
  21. package/dist/ai/voice/index.d.ts.map +1 -0
  22. package/dist/ai/voice/index.js +433 -0
  23. package/dist/ai/voice/index.js.map +1 -0
  24. package/dist/ai/voice/state.d.ts +34 -0
  25. package/dist/ai/voice/state.d.ts.map +1 -0
  26. package/dist/ai/voice/useVoiceSession.d.ts +82 -0
  27. package/dist/ai/voice/useVoiceSession.d.ts.map +1 -0
  28. package/dist/ai/voice/voiceCall.d.ts +57 -0
  29. package/dist/ai/voice/voiceCall.d.ts.map +1 -0
  30. package/dist/ai/voice/wire.d.ts +66 -0
  31. package/dist/ai/voice/wire.d.ts.map +1 -0
  32. package/dist/ai/voiceConfig.d.ts +70 -0
  33. package/dist/ai/voiceConfig.d.ts.map +1 -0
  34. package/dist/ai/voiceMcp.d.ts +89 -0
  35. package/dist/ai/voiceMcp.d.ts.map +1 -0
  36. package/dist/ai/voiceStore.d.ts +52 -0
  37. package/dist/ai/voiceStore.d.ts.map +1 -0
  38. package/dist/app/index.js +1 -1
  39. package/dist/bun/plugin.js +1 -1
  40. package/dist/bun/preload.js +1 -1
  41. package/dist/{chunk-4gmhx2ba.js → chunk-0eamc2tr.js} +2 -2
  42. package/dist/{chunk-4gmhx2ba.js.map → chunk-0eamc2tr.js.map} +1 -1
  43. package/dist/{chunk-2x0n3ze1.js → chunk-0gg4e94v.js} +2 -2
  44. package/dist/{chunk-2x0n3ze1.js.map → chunk-0gg4e94v.js.map} +1 -1
  45. package/dist/{chunk-p642ja42.js → chunk-0rhf4g12.js} +3 -3
  46. package/dist/{chunk-p642ja42.js.map → chunk-0rhf4g12.js.map} +1 -1
  47. package/dist/{chunk-dxm5gxqk.js → chunk-1awxdnb6.js} +1 -1
  48. package/dist/{chunk-2senwsdq.js → chunk-2cnawkke.js} +12 -12
  49. package/dist/{chunk-2senwsdq.js.map → chunk-2cnawkke.js.map} +3 -3
  50. package/dist/{chunk-fxm8fqh9.js → chunk-33hdxe7v.js} +1 -1
  51. package/dist/{chunk-63xekerg.js → chunk-3g5q0cj5.js} +2 -2
  52. package/dist/{chunk-63xekerg.js.map → chunk-3g5q0cj5.js.map} +1 -1
  53. package/dist/{chunk-xjs72pb0.js → chunk-3h37f124.js} +2 -2
  54. package/dist/{chunk-xjs72pb0.js.map → chunk-3h37f124.js.map} +1 -1
  55. package/dist/{chunk-65fq5a9d.js → chunk-4278e429.js} +2 -2
  56. package/dist/{chunk-65fq5a9d.js.map → chunk-4278e429.js.map} +1 -1
  57. package/dist/chunk-47e7fbsp.js +5 -0
  58. package/dist/{chunk-mvka90rq.js.map → chunk-47e7fbsp.js.map} +1 -1
  59. package/dist/{chunk-p6b0cyf8.js → chunk-4pvv8qs0.js} +2 -2
  60. package/dist/{chunk-p6b0cyf8.js.map → chunk-4pvv8qs0.js.map} +1 -1
  61. package/dist/{chunk-rjyw1thg.js → chunk-4t41m5f1.js} +2 -2
  62. package/dist/{chunk-rjyw1thg.js.map → chunk-4t41m5f1.js.map} +1 -1
  63. package/dist/{chunk-f21v4n2v.js → chunk-53rr32zm.js} +1 -1
  64. package/dist/{chunk-xhvknqvn.js → chunk-5pdgh9a1.js} +2 -2
  65. package/dist/{chunk-xhvknqvn.js.map → chunk-5pdgh9a1.js.map} +1 -1
  66. package/dist/{chunk-e4ay1vg9.js → chunk-65j32dtn.js} +2 -2
  67. package/dist/{chunk-e4ay1vg9.js.map → chunk-65j32dtn.js.map} +1 -1
  68. package/dist/{chunk-jxvec7jc.js → chunk-6fr6j338.js} +2 -2
  69. package/dist/{chunk-jxvec7jc.js.map → chunk-6fr6j338.js.map} +1 -1
  70. package/dist/{chunk-jd1afbrs.js → chunk-6hz63ts4.js} +4 -4
  71. package/dist/{chunk-jd1afbrs.js.map → chunk-6hz63ts4.js.map} +1 -1
  72. package/dist/{chunk-4z0rt6ws.js → chunk-6qty7at2.js} +2 -2
  73. package/dist/{chunk-4z0rt6ws.js.map → chunk-6qty7at2.js.map} +1 -1
  74. package/dist/{chunk-pzb3agv9.js → chunk-7nxnyw7x.js} +2 -2
  75. package/dist/{chunk-pzb3agv9.js.map → chunk-7nxnyw7x.js.map} +1 -1
  76. package/dist/{chunk-7n0zn5h4.js → chunk-8ryrjtn0.js} +2 -2
  77. package/dist/{chunk-7n0zn5h4.js.map → chunk-8ryrjtn0.js.map} +1 -1
  78. package/dist/{chunk-c1gq2ttb.js → chunk-8sde8337.js} +2 -2
  79. package/dist/{chunk-c1gq2ttb.js.map → chunk-8sde8337.js.map} +1 -1
  80. package/dist/{chunk-cggywjzz.js → chunk-99a4e6zf.js} +2 -2
  81. package/dist/{chunk-cggywjzz.js.map → chunk-99a4e6zf.js.map} +1 -1
  82. package/dist/chunk-b6yb3svm.js +6 -0
  83. package/dist/{chunk-539w9yt3.js.map → chunk-b6yb3svm.js.map} +2 -2
  84. package/dist/{chunk-822p54vc.js → chunk-beb265c9.js} +2 -2
  85. package/dist/{chunk-822p54vc.js.map → chunk-beb265c9.js.map} +1 -1
  86. package/dist/{chunk-hdgbh8hn.js → chunk-cxsvzqd4.js} +2 -2
  87. package/dist/{chunk-hdgbh8hn.js.map → chunk-cxsvzqd4.js.map} +1 -1
  88. package/dist/{chunk-pahr13xg.js → chunk-dt95z6et.js} +2 -2
  89. package/dist/{chunk-pahr13xg.js.map → chunk-dt95z6et.js.map} +1 -1
  90. package/dist/{chunk-hr8annr1.js → chunk-feajsgns.js} +2 -2
  91. package/dist/{chunk-hr8annr1.js.map → chunk-feajsgns.js.map} +1 -1
  92. package/dist/{chunk-2tda49mv.js → chunk-ff60bzvf.js} +2 -2
  93. package/dist/{chunk-2tda49mv.js.map → chunk-ff60bzvf.js.map} +1 -1
  94. package/dist/{chunk-b00z33zg.js → chunk-gng5e7pd.js} +1 -1
  95. package/dist/{chunk-mz73066c.js → chunk-gqgpwb88.js} +2 -2
  96. package/dist/{chunk-mz73066c.js.map → chunk-gqgpwb88.js.map} +1 -1
  97. package/dist/{chunk-h0xye69a.js → chunk-ha15x5ns.js} +2 -2
  98. package/dist/{chunk-h0xye69a.js.map → chunk-ha15x5ns.js.map} +1 -1
  99. package/dist/{chunk-d5g1caxb.js → chunk-hb9hm8za.js} +3 -3
  100. package/dist/{chunk-d5g1caxb.js.map → chunk-hb9hm8za.js.map} +1 -1
  101. package/dist/{chunk-8fvt7kys.js → chunk-hf492gg2.js} +1 -1
  102. package/dist/{chunk-fgtr9cds.js → chunk-hj88dcss.js} +3 -3
  103. package/dist/{chunk-fgtr9cds.js.map → chunk-hj88dcss.js.map} +1 -1
  104. package/dist/{chunk-hnf11jhc.js → chunk-j5g7qc5f.js} +2 -2
  105. package/dist/{chunk-hnf11jhc.js.map → chunk-j5g7qc5f.js.map} +1 -1
  106. package/dist/{chunk-e1fhy2h5.js → chunk-jgx8ggbz.js} +2 -2
  107. package/dist/{chunk-e1fhy2h5.js.map → chunk-jgx8ggbz.js.map} +1 -1
  108. package/dist/{chunk-35zw2s61.js → chunk-jx2mgvvt.js} +3 -3
  109. package/dist/{chunk-35zw2s61.js.map → chunk-jx2mgvvt.js.map} +1 -1
  110. package/dist/{chunk-w26cw41m.js → chunk-jx5zx48j.js} +2 -2
  111. package/dist/{chunk-w26cw41m.js.map → chunk-jx5zx48j.js.map} +1 -1
  112. package/dist/{chunk-z1gjgr1c.js → chunk-jy9dkgt0.js} +2 -2
  113. package/dist/{chunk-z1gjgr1c.js.map → chunk-jy9dkgt0.js.map} +1 -1
  114. package/dist/{chunk-7j48ymk1.js → chunk-k2xeskp6.js} +2 -2
  115. package/dist/{chunk-7j48ymk1.js.map → chunk-k2xeskp6.js.map} +1 -1
  116. package/dist/{chunk-jxmc33m9.js → chunk-ka991yx4.js} +2 -2
  117. package/dist/{chunk-jxmc33m9.js.map → chunk-ka991yx4.js.map} +1 -1
  118. package/dist/{chunk-65s63bdh.js → chunk-krk880my.js} +2 -2
  119. package/dist/{chunk-65s63bdh.js.map → chunk-krk880my.js.map} +1 -1
  120. package/dist/{chunk-x4yq0c9v.js → chunk-m9vwg9j1.js} +3 -3
  121. package/dist/{chunk-x4yq0c9v.js.map → chunk-m9vwg9j1.js.map} +1 -1
  122. package/dist/{chunk-x07rp3fc.js → chunk-mpqvmge4.js} +1 -1
  123. package/dist/{chunk-n7sbwpce.js → chunk-mw7ft5bs.js} +2 -2
  124. package/dist/{chunk-n7sbwpce.js.map → chunk-mw7ft5bs.js.map} +1 -1
  125. package/dist/{chunk-v5esvs66.js → chunk-njpnstck.js} +2 -2
  126. package/dist/{chunk-v5esvs66.js.map → chunk-njpnstck.js.map} +1 -1
  127. package/dist/{chunk-p320fd3g.js → chunk-p82868xx.js} +2 -2
  128. package/dist/{chunk-p320fd3g.js.map → chunk-p82868xx.js.map} +1 -1
  129. package/dist/{chunk-jbd723h1.js → chunk-pm8wjatv.js} +3 -3
  130. package/dist/{chunk-jbd723h1.js.map → chunk-pm8wjatv.js.map} +1 -1
  131. package/dist/{chunk-t227jn4b.js → chunk-q15f5g86.js} +2 -2
  132. package/dist/{chunk-t227jn4b.js.map → chunk-q15f5g86.js.map} +1 -1
  133. package/dist/{chunk-838cdh6v.js → chunk-qfra7gah.js} +2 -2
  134. package/dist/{chunk-838cdh6v.js.map → chunk-qfra7gah.js.map} +1 -1
  135. package/dist/{chunk-aprjvzrz.js → chunk-rnp5k8m3.js} +2 -2
  136. package/dist/{chunk-aprjvzrz.js.map → chunk-rnp5k8m3.js.map} +1 -1
  137. package/dist/{chunk-qz89m7ss.js → chunk-rzqft8q8.js} +2 -2
  138. package/dist/{chunk-qz89m7ss.js.map → chunk-rzqft8q8.js.map} +1 -1
  139. package/dist/{chunk-zc5962gh.js → chunk-s025aycr.js} +2 -2
  140. package/dist/{chunk-zc5962gh.js.map → chunk-s025aycr.js.map} +1 -1
  141. package/dist/{chunk-y14weak4.js → chunk-s21kn0x8.js} +2 -2
  142. package/dist/{chunk-y14weak4.js.map → chunk-s21kn0x8.js.map} +1 -1
  143. package/dist/{chunk-3wnczt5w.js → chunk-s55t7rxw.js} +2 -2
  144. package/dist/{chunk-3wnczt5w.js.map → chunk-s55t7rxw.js.map} +1 -1
  145. package/dist/{chunk-fzs7g6mc.js → chunk-s782bzeq.js} +2 -2
  146. package/dist/{chunk-fzs7g6mc.js.map → chunk-s782bzeq.js.map} +1 -1
  147. package/dist/{chunk-77ymcf7q.js → chunk-sa6cxt3g.js} +2 -2
  148. package/dist/{chunk-77ymcf7q.js.map → chunk-sa6cxt3g.js.map} +1 -1
  149. package/dist/{chunk-dz76qhx8.js → chunk-swwkdm75.js} +2 -2
  150. package/dist/{chunk-dz76qhx8.js.map → chunk-swwkdm75.js.map} +1 -1
  151. package/dist/{chunk-58mg4rpd.js → chunk-t33yh84d.js} +2 -2
  152. package/dist/{chunk-58mg4rpd.js.map → chunk-t33yh84d.js.map} +1 -1
  153. package/dist/{chunk-mym7egdq.js → chunk-vnw91fe1.js} +2 -2
  154. package/dist/{chunk-mym7egdq.js.map → chunk-vnw91fe1.js.map} +1 -1
  155. package/dist/{chunk-4wda9teq.js → chunk-w1cksc5z.js} +3 -3
  156. package/dist/{chunk-4wda9teq.js.map → chunk-w1cksc5z.js.map} +1 -1
  157. package/dist/chunk-wevj4981.js +122 -0
  158. package/dist/{chunk-fx7r6pdf.js.map → chunk-wevj4981.js.map} +3 -3
  159. package/dist/{chunk-ayjfs5mm.js → chunk-wrsf2abe.js} +2 -2
  160. package/dist/{chunk-ayjfs5mm.js.map → chunk-wrsf2abe.js.map} +1 -1
  161. package/dist/{chunk-j1wc4afr.js → chunk-x16ckgq1.js} +2 -2
  162. package/dist/{chunk-j1wc4afr.js.map → chunk-x16ckgq1.js.map} +1 -1
  163. package/dist/{chunk-gnaxedwh.js → chunk-x51tegk3.js} +2 -2
  164. package/dist/{chunk-gnaxedwh.js.map → chunk-x51tegk3.js.map} +1 -1
  165. package/dist/{chunk-qspam5cf.js → chunk-x5hj0mqp.js} +2 -2
  166. package/dist/{chunk-qspam5cf.js.map → chunk-x5hj0mqp.js.map} +1 -1
  167. package/dist/{chunk-dp2yz8bj.js → chunk-x9bk5s21.js} +2 -2
  168. package/dist/{chunk-dp2yz8bj.js.map → chunk-x9bk5s21.js.map} +1 -1
  169. package/dist/{chunk-p462p2xe.js → chunk-xvvsc58s.js} +2 -2
  170. package/dist/{chunk-p462p2xe.js.map → chunk-xvvsc58s.js.map} +1 -1
  171. package/dist/{chunk-0g5f8ymy.js → chunk-xymr3asa.js} +2 -2
  172. package/dist/{chunk-0g5f8ymy.js.map → chunk-xymr3asa.js.map} +1 -1
  173. package/dist/{chunk-j4y0sjdc.js → chunk-ysc7ykss.js} +2 -2
  174. package/dist/{chunk-j4y0sjdc.js.map → chunk-ysc7ykss.js.map} +1 -1
  175. package/dist/{chunk-3xy41vc0.js → chunk-z61dj480.js} +3 -3
  176. package/dist/{chunk-3xy41vc0.js.map → chunk-z61dj480.js.map} +1 -1
  177. package/dist/{chunk-0q68t71d.js → chunk-z9xxthp1.js} +3 -3
  178. package/dist/{chunk-0q68t71d.js.map → chunk-z9xxthp1.js.map} +1 -1
  179. package/dist/{chunk-47eav3ex.js → chunk-zv5qyrqg.js} +2 -2
  180. package/dist/{chunk-47eav3ex.js.map → chunk-zv5qyrqg.js.map} +1 -1
  181. package/dist/{chunk-asmszzkp.js → chunk-zyhmn3hy.js} +3 -3
  182. package/dist/{chunk-asmszzkp.js.map → chunk-zyhmn3hy.js.map} +1 -1
  183. package/dist/{chunk-zzgt759n.js → chunk-zypmy8yq.js} +2 -2
  184. package/dist/{chunk-zzgt759n.js.map → chunk-zypmy8yq.js.map} +1 -1
  185. package/dist/config/index.js +1 -1
  186. package/dist/console/run.js +2 -2
  187. package/dist/console/run.js.map +1 -1
  188. package/dist/container/index.js +2 -2
  189. package/dist/container/index.js.map +1 -1
  190. package/dist/database/index.js +2 -2
  191. package/dist/database/index.js.map +1 -1
  192. package/dist/email/index.js +2 -2
  193. package/dist/email/index.js.map +1 -1
  194. package/dist/facades/index.js +2 -2
  195. package/dist/facades/index.js.map +1 -1
  196. package/dist/foundation/index.js +2 -2
  197. package/dist/foundation/index.js.map +1 -1
  198. package/dist/http/ApiRouter.d.ts +27 -5
  199. package/dist/http/ApiRouter.d.ts.map +1 -1
  200. package/dist/http/index.js +2 -2
  201. package/dist/http/index.js.map +1 -1
  202. package/dist/i18n/dictionaryRuntime.js +2 -2
  203. package/dist/i18n/dictionaryRuntime.js.map +1 -1
  204. package/dist/i18n/index.js +2 -2
  205. package/dist/i18n/index.js.map +1 -1
  206. package/dist/kernel/index.js +2 -2
  207. package/dist/kernel/index.js.map +1 -1
  208. package/dist/orm/defaults.d.ts.map +1 -1
  209. package/dist/orm/index.js +3 -3
  210. package/dist/orm/index.js.map +3 -3
  211. package/dist/server/index.js +2 -2
  212. package/dist/server/index.js.map +1 -1
  213. package/dist/services/index.js +2 -2
  214. package/dist/services/index.js.map +1 -1
  215. package/dist/services/queue/work.js +2 -2
  216. package/dist/services/queue/work.js.map +1 -1
  217. package/dist/stats/route-table.js +1 -1
  218. package/dist/support/index.js +2 -2
  219. package/dist/support/index.js.map +1 -1
  220. package/dist/vitest/index.js +1 -1
  221. package/package.json +2 -1
  222. package/dist/chunk-539w9yt3.js +0 -6
  223. package/dist/chunk-fx7r6pdf.js +0 -122
  224. package/dist/chunk-mvka90rq.js +0 -5
  225. /package/dist/{chunk-dxm5gxqk.js.map → chunk-1awxdnb6.js.map} +0 -0
  226. /package/dist/{chunk-fxm8fqh9.js.map → chunk-33hdxe7v.js.map} +0 -0
  227. /package/dist/{chunk-f21v4n2v.js.map → chunk-53rr32zm.js.map} +0 -0
  228. /package/dist/{chunk-b00z33zg.js.map → chunk-gng5e7pd.js.map} +0 -0
  229. /package/dist/{chunk-8fvt7kys.js.map → chunk-hf492gg2.js.map} +0 -0
  230. /package/dist/{chunk-x07rp3fc.js.map → chunk-mpqvmge4.js.map} +0 -0
@@ -0,0 +1,23 @@
1
+ /**
2
+ * `gemi/ai/voice` — the browser half of voice calls to an agent (#921).
3
+ *
4
+ * An entry of its own, beside `gemi/ai/client`, so an app that never uses
5
+ * voice never bundles it, and an app that does gets it in the chunk of the view
6
+ * (or the dynamically imported component) that uses it, never in the entry
7
+ * chunk every route loads. The packaging test checks that.
8
+ *
9
+ * One module graph rather than a hook plus a further `import()` for the peer
10
+ * connection: the whole entry is a few kilobytes and is already only loaded
11
+ * where it is used, so a second split would add a request for no saving. It
12
+ * also imports nothing from `gemi/client` (not even `useParams`), because a
13
+ * module both share would be moved out of the app's entry chunk into a shared
14
+ * chunk that every route then loads.
15
+ *
16
+ * Like `gemi/ai/client`, nothing here reaches the server half: `useVoiceSession`
17
+ * talks to the app's `this.voice(Controller)` route, which holds the vendor
18
+ * key. The server half (`VoiceBridge`, `VoiceConfig`) is in `gemi/ai`.
19
+ */
20
+ export { useVoiceSession } from "./useVoiceSession";
21
+ export type { UseVoiceSessionParams, UseVoiceSessionResult } from "./useVoiceSession";
22
+ export type { VoiceCaption, VoiceStatus } from "./wire";
23
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../ai/voice/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,YAAY,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AACtF,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC"}
@@ -0,0 +1,433 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ //#region ai/voice/state.ts
3
+ function initialVoiceState() {
4
+ return {
5
+ phase: "idle",
6
+ userSpeaking: false,
7
+ thinking: false,
8
+ speaking: false,
9
+ working: [],
10
+ captions: []
11
+ };
12
+ }
13
+ function voiceStatus(state) {
14
+ if (state.phase === "idle") return "idle";
15
+ if (state.phase === "connecting") return "connecting";
16
+ if (state.phase === "ended") return "ended";
17
+ if (state.speaking) return "speaking";
18
+ if (state.userSpeaking) return "listening";
19
+ if (state.thinking) return "thinking";
20
+ if (state.working.length) return "working";
21
+ return "listening";
22
+ }
23
+ /** One data-channel event onto the state. `delegate` is the delegate tool's name. */
24
+ function applyVoiceEvent(state, event, delegate) {
25
+ switch (event.type) {
26
+ case "input_audio_buffer.speech_started": return {
27
+ ...state,
28
+ userSpeaking: true,
29
+ thinking: false
30
+ };
31
+ case "input_audio_buffer.speech_stopped":
32
+ case "input_audio_buffer.committed": return {
33
+ ...state,
34
+ userSpeaking: false,
35
+ thinking: true
36
+ };
37
+ case "output_audio_buffer.started": return {
38
+ ...state,
39
+ speaking: true,
40
+ thinking: false
41
+ };
42
+ case "output_audio_buffer.stopped":
43
+ case "output_audio_buffer.cleared": return {
44
+ ...state,
45
+ speaking: false
46
+ };
47
+ case "response.done": {
48
+ let next = {
49
+ ...state,
50
+ thinking: false
51
+ };
52
+ for (const item of event.response?.output ?? []) if (item?.type === "function_call" && item.name === delegate && item.call_id) next = addWorking(next, item.call_id);
53
+ return next;
54
+ }
55
+ case "response.function_call_arguments.done": return event.name === delegate && event.call_id ? addWorking(state, event.call_id) : state;
56
+ case "conversation.item.added":
57
+ case "conversation.item.created":
58
+ case "conversation.item.done": {
59
+ const item = event.item;
60
+ if (item?.type === "function_call_output" && state.working.includes(item.call_id)) return {
61
+ ...state,
62
+ working: state.working.filter((id) => id !== item.call_id)
63
+ };
64
+ return state;
65
+ }
66
+ case "conversation.item.input_audio_transcription.delta": return caption(state, event.item_id, "user", event.delta ?? "", false);
67
+ case "conversation.item.input_audio_transcription.completed": return caption(state, event.item_id, "user", event.transcript ?? "", true);
68
+ case "response.output_audio_transcript.delta":
69
+ case "response.audio_transcript.delta": return caption(state, event.item_id, "assistant", event.delta ?? "", false);
70
+ case "response.output_audio_transcript.done":
71
+ case "response.audio_transcript.done": return caption(state, event.item_id, "assistant", event.transcript ?? "", true);
72
+ default: return state;
73
+ }
74
+ }
75
+ function addWorking(state, callId) {
76
+ return state.working.includes(callId) ? state : {
77
+ ...state,
78
+ working: [...state.working, callId]
79
+ };
80
+ }
81
+ /** A delta appends to its line; a final text replaces it. */
82
+ function caption(state, id, role, text, final) {
83
+ if (!id) return state;
84
+ const index = state.captions.findIndex((line) => line.id === id);
85
+ const captions = [...state.captions];
86
+ if (index === -1) {
87
+ if (!text) return state;
88
+ captions.push({
89
+ id,
90
+ role,
91
+ text,
92
+ final
93
+ });
94
+ } else {
95
+ const line = captions[index];
96
+ captions[index] = {
97
+ ...line,
98
+ text: final ? text || line.text : line.text + text,
99
+ final
100
+ };
101
+ }
102
+ return {
103
+ ...state,
104
+ captions: captions.slice(-20)
105
+ };
106
+ }
107
+ //#endregion
108
+ //#region ai/voice/voiceCall.ts
109
+ var VoiceCallError = class extends Error {
110
+ status;
111
+ code;
112
+ constructor(message, options = {}) {
113
+ super(message, { cause: options.cause });
114
+ this.name = "VoiceCallError";
115
+ this.status = options.status;
116
+ this.code = options.code;
117
+ }
118
+ };
119
+ async function startVoiceCall(options) {
120
+ if (typeof RTCPeerConnection === "undefined" || !navigator.mediaDevices?.getUserMedia) throw new VoiceCallError("This browser can't make voice calls.", { code: "unsupported" });
121
+ let stream;
122
+ try {
123
+ stream = await navigator.mediaDevices.getUserMedia({ audio: {
124
+ echoCancellation: true,
125
+ noiseSuppression: true,
126
+ autoGainControl: true
127
+ } });
128
+ } catch (error) {
129
+ throw new VoiceCallError("The microphone is not available.", {
130
+ code: "microphone_denied",
131
+ cause: error
132
+ });
133
+ }
134
+ const track = stream.getAudioTracks()[0];
135
+ let pushToTalk = options.pushToTalk === true;
136
+ let muted = false;
137
+ track.enabled = !pushToTalk;
138
+ const pc = new RTCPeerConnection();
139
+ const audio = document.createElement("audio");
140
+ audio.autoplay = true;
141
+ pc.ontrack = (event) => {
142
+ audio.srcObject = event.streams[0] ?? new MediaStream([event.track]);
143
+ };
144
+ pc.addTrack(track, stream);
145
+ const channel = pc.createDataChannel("oai-events");
146
+ let closed = false;
147
+ const close = (error) => {
148
+ if (closed) return;
149
+ closed = true;
150
+ try {
151
+ channel.close();
152
+ } catch {}
153
+ pc.close();
154
+ for (const t of stream.getTracks()) t.stop();
155
+ audio.srcObject = null;
156
+ options.onClose(error);
157
+ };
158
+ channel.addEventListener("message", (message) => {
159
+ let event;
160
+ try {
161
+ event = JSON.parse(String(message.data));
162
+ } catch {
163
+ return;
164
+ }
165
+ if (event && typeof event === "object" && typeof event.type === "string") options.onEvent(event);
166
+ });
167
+ channel.addEventListener("close", () => close());
168
+ pc.addEventListener("connectionstatechange", () => {
169
+ if (pc.connectionState === "failed") close(new VoiceCallError("The voice connection failed."));
170
+ if (pc.connectionState === "closed") close();
171
+ });
172
+ const outbox = [];
173
+ channel.addEventListener("open", () => {
174
+ for (const data of outbox.splice(0)) channel.send(data);
175
+ });
176
+ const send = (event) => {
177
+ const data = JSON.stringify(event);
178
+ if (channel.readyState === "open") channel.send(data);
179
+ else if (channel.readyState === "connecting") outbox.push(data);
180
+ };
181
+ const request = (method, url, payload, init = {}) => fetch(url, {
182
+ method,
183
+ headers: {
184
+ "Content-Type": "application/json",
185
+ ...options.headers
186
+ },
187
+ ...payload === void 0 ? {} : { body: JSON.stringify(payload) },
188
+ ...init
189
+ });
190
+ let started;
191
+ try {
192
+ const offer = await pc.createOffer();
193
+ await pc.setLocalDescription(offer);
194
+ const response = await request("POST", options.url, {
195
+ ...options.body && typeof options.body === "object" ? options.body : {},
196
+ sdp: offer.sdp,
197
+ ...options.threadId ? { threadId: options.threadId } : {},
198
+ ...pushToTalk ? { pushToTalk: true } : {}
199
+ });
200
+ if (!response.ok) {
201
+ const error = await response.json().catch(() => null);
202
+ throw new VoiceCallError(error?.error?.message ?? `The voice call could not be started (${response.status}).`, {
203
+ status: response.status,
204
+ code: error?.error?.code
205
+ });
206
+ }
207
+ started = await response.json();
208
+ await pc.setRemoteDescription({
209
+ type: "answer",
210
+ sdp: started.sdp
211
+ });
212
+ } catch (error) {
213
+ close();
214
+ throw error instanceof VoiceCallError ? error : new VoiceCallError("The voice call could not be started.", { cause: error });
215
+ }
216
+ const sessionUrl = `${options.url}/${encodeURIComponent(started.sessionId)}`;
217
+ const applyTrack = () => {
218
+ track.enabled = !muted && !pushToTalk;
219
+ };
220
+ return {
221
+ sessionId: started.sessionId,
222
+ threadId: started.threadId,
223
+ delegate: started.delegate,
224
+ endsAt: started.endsAt,
225
+ mute(on) {
226
+ muted = on;
227
+ applyTrack();
228
+ },
229
+ setPushToTalk(on) {
230
+ pushToTalk = on;
231
+ applyTrack();
232
+ send({
233
+ type: "session.update",
234
+ session: {
235
+ type: "realtime",
236
+ audio: { input: { turn_detection: on ? null : options.turnDetection ?? { type: "semantic_vad" } } }
237
+ }
238
+ });
239
+ },
240
+ press() {
241
+ if (!pushToTalk || muted) return;
242
+ send({ type: "input_audio_buffer.clear" });
243
+ track.enabled = true;
244
+ },
245
+ release() {
246
+ if (!pushToTalk) return;
247
+ track.enabled = false;
248
+ send({ type: "input_audio_buffer.commit" });
249
+ send({ type: "response.create" });
250
+ },
251
+ async setBody(body) {
252
+ const response = await request("PATCH", sessionUrl, body ?? {});
253
+ if (!response.ok && response.status !== 409) throw new VoiceCallError(`The voice session could not be updated (${response.status}).`, { status: response.status });
254
+ },
255
+ async end() {
256
+ try {
257
+ await request("DELETE", sessionUrl, void 0, { keepalive: true });
258
+ } finally {
259
+ close();
260
+ }
261
+ },
262
+ close: () => close()
263
+ };
264
+ }
265
+ //#endregion
266
+ //#region ai/voice/useVoiceSession.ts
267
+ /**
268
+ * A voice call to an agent, for a route mounted with `this.voice(Controller)`
269
+ * (#921).
270
+ *
271
+ * ```tsx
272
+ * const voice = useVoiceSession("/page-builder/voice", { threadId, body: { pageId } });
273
+ * <button onClick={() => (voice.status === "idle" ? voice.start() : voice.end())}>
274
+ * {voice.status}
275
+ * </button>
276
+ * ```
277
+ *
278
+ * Import it from `gemi/ai/voice`, an entry of its own, so an app that never
279
+ * uses voice never bundles it, and use it from a view or a dynamically imported
280
+ * component, so its code is not in the entry chunk.
281
+ *
282
+ * @experimental Not yet verified against a live call.
283
+ */
284
+ function useVoiceSession(path, params = {}) {
285
+ const routeParams = params.params;
286
+ const url = useMemo(() => {
287
+ let out = String(path).replace(/^[A-Z]+:/, "");
288
+ for (const [key, value] of Object.entries(routeParams ?? {})) out = out.replace(`:${key}?`, value).replace(`:${key}`, value);
289
+ return `/api${out}`;
290
+ }, [path, routeParams && JSON.stringify(routeParams)]);
291
+ const [state, setState] = useState(initialVoiceState);
292
+ const [error, setError] = useState(null);
293
+ const [muted, setMuted] = useState(false);
294
+ const [pushToTalk, setPushToTalkState] = useState(params.pushToTalk === true);
295
+ const [call, setCall] = useState(null);
296
+ const callRef = useRef(null);
297
+ /** A `start()` in flight: a second click must not start a second call. */
298
+ const startingRef = useRef(false);
299
+ const latest = useRef(params);
300
+ latest.current = params;
301
+ const delegateRef = useRef("ask_agent");
302
+ const mounted = useRef(true);
303
+ useEffect(() => {
304
+ mounted.current = true;
305
+ return () => {
306
+ mounted.current = false;
307
+ callRef.current?.end().catch(() => void 0);
308
+ callRef.current = null;
309
+ };
310
+ }, []);
311
+ const start = useCallback(async () => {
312
+ if (callRef.current || startingRef.current) return;
313
+ startingRef.current = true;
314
+ setError(null);
315
+ setState({
316
+ ...initialVoiceState(),
317
+ phase: "connecting"
318
+ });
319
+ try {
320
+ const { threadId, body, headers } = latest.current;
321
+ const started = await startVoiceCall({
322
+ url,
323
+ threadId,
324
+ body,
325
+ headers,
326
+ pushToTalk,
327
+ onEvent: (event) => {
328
+ if (!mounted.current) return;
329
+ setState((current) => current.phase === "connecting" ? applyVoiceEvent({
330
+ ...current,
331
+ phase: "live"
332
+ }, event, delegateRef.current) : applyVoiceEvent(current, event, delegateRef.current));
333
+ },
334
+ onClose: (closeError) => {
335
+ callRef.current = null;
336
+ if (!mounted.current) return;
337
+ setCall(null);
338
+ setState((current) => ({
339
+ ...current,
340
+ phase: "ended",
341
+ speaking: false,
342
+ thinking: false
343
+ }));
344
+ if (closeError) {
345
+ setError(closeError);
346
+ latest.current.onError?.(closeError);
347
+ }
348
+ latest.current.onEnd?.();
349
+ }
350
+ });
351
+ delegateRef.current = started.delegate;
352
+ if (!mounted.current) {
353
+ await started.end().catch(() => void 0);
354
+ return;
355
+ }
356
+ callRef.current = started;
357
+ setCall(started);
358
+ setState((current) => current.phase === "connecting" ? {
359
+ ...current,
360
+ phase: "live"
361
+ } : current);
362
+ } catch (failure) {
363
+ const reported = failure instanceof Error ? failure : new Error(String(failure));
364
+ if (!mounted.current) return;
365
+ setState((current) => ({
366
+ ...current,
367
+ phase: "ended"
368
+ }));
369
+ setError(reported);
370
+ latest.current.onError?.(reported);
371
+ } finally {
372
+ startingRef.current = false;
373
+ }
374
+ }, [url, pushToTalk]);
375
+ const end = useCallback(async () => {
376
+ const current = callRef.current;
377
+ callRef.current = null;
378
+ await current?.end();
379
+ }, []);
380
+ const mute = useCallback((on) => {
381
+ setMuted((was) => {
382
+ const next = on ?? !was;
383
+ callRef.current?.mute(next);
384
+ return next;
385
+ });
386
+ }, []);
387
+ const setPushToTalk = useCallback((on) => {
388
+ setPushToTalkState(on);
389
+ callRef.current?.setPushToTalk(on);
390
+ }, []);
391
+ const press = useCallback(() => {
392
+ callRef.current?.press();
393
+ setState((current) => ({
394
+ ...current,
395
+ userSpeaking: true
396
+ }));
397
+ }, []);
398
+ const release = useCallback(() => {
399
+ callRef.current?.release();
400
+ setState((current) => ({
401
+ ...current,
402
+ userSpeaking: false,
403
+ thinking: true
404
+ }));
405
+ }, []);
406
+ const setBody = useCallback(async (body) => {
407
+ latest.current = {
408
+ ...latest.current,
409
+ body
410
+ };
411
+ await callRef.current?.setBody(body);
412
+ }, []);
413
+ return {
414
+ status: voiceStatus(state),
415
+ captions: state.captions,
416
+ error,
417
+ muted,
418
+ pushToTalk,
419
+ endsAt: call?.endsAt,
420
+ sessionId: call?.sessionId,
421
+ start,
422
+ end,
423
+ mute,
424
+ setPushToTalk,
425
+ press,
426
+ release,
427
+ setBody
428
+ };
429
+ }
430
+ //#endregion
431
+ export { useVoiceSession };
432
+
433
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../ai/voice/state.ts","../../../ai/voice/voiceCall.ts","../../../ai/voice/useVoiceSession.ts"],"sourcesContent":["import type { VoiceCaption, VoiceStatus } from \"./wire\";\n\n/**\n * What `useVoiceSession` shows, as a pure reducer over the call's data-channel\n * events (#921). Separate from the hook so it is tested without a DOM, a\n * microphone or a peer connection.\n *\n * The events are the vendor's, as the data channel delivers them. Which ones\n * arrive there — in particular the `function_call_output` items the server's\n * sideband adds, which end `working` — follows the documented protocol and has\n * not been measured against Azure.\n */\nexport type VoiceState = {\n phase: \"idle\" | \"connecting\" | \"live\" | \"ended\";\n /** The user is speaking (VAD) or holding push-to-talk. */\n userSpeaking: boolean;\n /** The user stopped and no answer has started playing. */\n thinking: boolean;\n /** The voice's audio is playing. */\n speaking: boolean;\n /** Delegate calls with no output yet. */\n working: string[];\n captions: VoiceCaption[];\n};\n\nexport const MAX_CAPTIONS = 20;\n\nexport function initialVoiceState(): VoiceState {\n return {\n phase: \"idle\",\n userSpeaking: false,\n thinking: false,\n speaking: false,\n working: [],\n captions: [],\n };\n}\n\nexport function voiceStatus(state: VoiceState): VoiceStatus {\n if (state.phase === \"idle\") return \"idle\";\n if (state.phase === \"connecting\") return \"connecting\";\n if (state.phase === \"ended\") return \"ended\";\n if (state.speaking) return \"speaking\";\n if (state.userSpeaking) return \"listening\";\n if (state.thinking) return \"thinking\";\n if (state.working.length) return \"working\";\n return \"listening\";\n}\n\ntype Event = { type: string; [key: string]: any };\n\n/** One data-channel event onto the state. `delegate` is the delegate tool's name. */\nexport function applyVoiceEvent(state: VoiceState, event: Event, delegate: string): VoiceState {\n switch (event.type) {\n case \"input_audio_buffer.speech_started\":\n return { ...state, userSpeaking: true, thinking: false };\n case \"input_audio_buffer.speech_stopped\":\n case \"input_audio_buffer.committed\":\n return { ...state, userSpeaking: false, thinking: true };\n case \"output_audio_buffer.started\":\n return { ...state, speaking: true, thinking: false };\n case \"output_audio_buffer.stopped\":\n case \"output_audio_buffer.cleared\":\n return { ...state, speaking: false };\n case \"response.done\": {\n let next = { ...state, thinking: false };\n for (const item of event.response?.output ?? []) {\n if (item?.type === \"function_call\" && item.name === delegate && item.call_id) {\n next = addWorking(next, item.call_id);\n }\n }\n return next;\n }\n case \"response.function_call_arguments.done\":\n return event.name === delegate && event.call_id ? addWorking(state, event.call_id) : state;\n case \"conversation.item.added\":\n case \"conversation.item.created\":\n case \"conversation.item.done\": {\n const item = event.item;\n if (item?.type === \"function_call_output\" && state.working.includes(item.call_id)) {\n return { ...state, working: state.working.filter((id) => id !== item.call_id) };\n }\n return state;\n }\n case \"conversation.item.input_audio_transcription.delta\":\n return caption(state, event.item_id, \"user\", event.delta ?? \"\", false);\n case \"conversation.item.input_audio_transcription.completed\":\n return caption(state, event.item_id, \"user\", event.transcript ?? \"\", true);\n case \"response.output_audio_transcript.delta\":\n case \"response.audio_transcript.delta\":\n return caption(state, event.item_id, \"assistant\", event.delta ?? \"\", false);\n case \"response.output_audio_transcript.done\":\n case \"response.audio_transcript.done\":\n return caption(state, event.item_id, \"assistant\", event.transcript ?? \"\", true);\n default:\n return state;\n }\n}\n\nfunction addWorking(state: VoiceState, callId: string): VoiceState {\n return state.working.includes(callId) ? state : { ...state, working: [...state.working, callId] };\n}\n\n/** A delta appends to its line; a final text replaces it. */\nfunction caption(\n state: VoiceState,\n id: string | undefined,\n role: VoiceCaption[\"role\"],\n text: string,\n final: boolean,\n): VoiceState {\n if (!id) return state;\n const index = state.captions.findIndex((line) => line.id === id);\n const captions = [...state.captions];\n if (index === -1) {\n if (!text) return state;\n captions.push({ id, role, text, final });\n } else {\n const line = captions[index]!;\n captions[index] = { ...line, text: final ? text || line.text : line.text + text, final };\n }\n return { ...state, captions: captions.slice(-MAX_CAPTIONS) };\n}\n","import type { VoiceStartResponse } from \"./wire\";\n\n/**\n * The browser's half of a voice call (#921): the microphone, the peer\n * connection, the vendor's data channel and the app's three routes.\n *\n * Only `gemi/ai/voice` reaches it, so it is in an app's bundle only where the\n * app uses that entry. The packaging test checks it stays out of the entry\n * chunk.\n *\n * No vendor SDK: a raw `RTCPeerConnection`, with the audio going straight to\n * the vendor and the SDP offer going to the app's route, which creates the\n * call with the server's key. The browser never holds a key or a secret.\n */\n\nexport type VoiceCallOptions = {\n /** `/api/<route>`. */\n url: string;\n threadId?: string;\n body?: unknown;\n pushToTalk?: boolean;\n headers?: Record<string, string>;\n /** Every vendor event from the data channel. */\n onEvent: (event: { type: string; [key: string]: unknown }) => void;\n /** The connection closed or failed. */\n onClose: (error?: Error) => void;\n /** What push-to-talk turns detection back on to. Default semantic VAD. */\n turnDetection?: Record<string, unknown>;\n};\n\nexport type VoiceCall = {\n sessionId: string;\n threadId: string;\n delegate: string;\n endsAt?: number;\n mute(muted: boolean): void;\n setPushToTalk(on: boolean): void;\n /** Push-to-talk: start speaking. */\n press(): void;\n /** Push-to-talk: done speaking; the turn is committed and answered. */\n release(): void;\n setBody(body: unknown): Promise<void>;\n /** Hangs up through the app's route and closes everything. */\n end(): Promise<void>;\n /** Closes everything without telling the server (it learns from the vendor). */\n close(): void;\n};\n\nexport class VoiceCallError extends Error {\n readonly status?: number;\n readonly code?: string;\n\n constructor(message: string, options: { status?: number; code?: string; cause?: unknown } = {}) {\n super(message, { cause: options.cause });\n this.name = \"VoiceCallError\";\n this.status = options.status;\n this.code = options.code;\n }\n}\n\nexport async function startVoiceCall(options: VoiceCallOptions): Promise<VoiceCall> {\n if (typeof RTCPeerConnection === \"undefined\" || !navigator.mediaDevices?.getUserMedia) {\n throw new VoiceCallError(\"This browser can't make voice calls.\", { code: \"unsupported\" });\n }\n\n let stream: MediaStream;\n try {\n stream = await navigator.mediaDevices.getUserMedia({\n audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true },\n });\n } catch (error) {\n throw new VoiceCallError(\"The microphone is not available.\", {\n code: \"microphone_denied\",\n cause: error,\n });\n }\n const track = stream.getAudioTracks()[0]!;\n let pushToTalk = options.pushToTalk === true;\n let muted = false;\n track.enabled = !pushToTalk;\n\n const pc = new RTCPeerConnection();\n const audio = document.createElement(\"audio\");\n audio.autoplay = true;\n pc.ontrack = (event) => {\n audio.srcObject = event.streams[0] ?? new MediaStream([event.track]);\n };\n pc.addTrack(track, stream);\n const channel = pc.createDataChannel(\"oai-events\");\n\n let closed = false;\n const close = (error?: Error) => {\n if (closed) return;\n closed = true;\n try {\n channel.close();\n } catch {\n // already closed\n }\n pc.close();\n for (const t of stream.getTracks()) t.stop();\n audio.srcObject = null;\n options.onClose(error);\n };\n\n channel.addEventListener(\"message\", (message) => {\n let event: unknown;\n try {\n event = JSON.parse(String(message.data));\n } catch {\n return;\n }\n if (\n event &&\n typeof event === \"object\" &&\n typeof (event as { type?: unknown }).type === \"string\"\n ) {\n options.onEvent(event as { type: string });\n }\n });\n channel.addEventListener(\"close\", () => close());\n pc.addEventListener(\"connectionstatechange\", () => {\n if (pc.connectionState === \"failed\") close(new VoiceCallError(\"The voice connection failed.\"));\n if (pc.connectionState === \"closed\") close();\n });\n\n // Events sent before the data channel opens (push-to-talk pressed at once)\n // wait for it rather than being dropped.\n const outbox: string[] = [];\n channel.addEventListener(\"open\", () => {\n for (const data of outbox.splice(0)) channel.send(data);\n });\n const send = (event: Record<string, unknown>) => {\n const data = JSON.stringify(event);\n if (channel.readyState === \"open\") channel.send(data);\n else if (channel.readyState === \"connecting\") outbox.push(data);\n };\n const request = (method: string, url: string, payload?: unknown, init: RequestInit = {}) =>\n fetch(url, {\n method,\n headers: { \"Content-Type\": \"application/json\", ...options.headers },\n ...(payload === undefined ? {} : { body: JSON.stringify(payload) }),\n ...init,\n });\n\n let started: VoiceStartResponse;\n try {\n const offer = await pc.createOffer();\n await pc.setLocalDescription(offer);\n const response = await request(\"POST\", options.url, {\n ...(options.body && typeof options.body === \"object\" ? options.body : {}),\n sdp: offer.sdp,\n ...(options.threadId ? { threadId: options.threadId } : {}),\n ...(pushToTalk ? { pushToTalk: true } : {}),\n });\n if (!response.ok) {\n const error = await response.json().catch(() => null);\n throw new VoiceCallError(\n error?.error?.message ?? `The voice call could not be started (${response.status}).`,\n { status: response.status, code: error?.error?.code },\n );\n }\n started = await response.json();\n await pc.setRemoteDescription({ type: \"answer\", sdp: started.sdp });\n } catch (error) {\n close();\n throw error instanceof VoiceCallError\n ? error\n : new VoiceCallError(\"The voice call could not be started.\", { cause: error });\n }\n\n const sessionUrl = `${options.url}/${encodeURIComponent(started.sessionId)}`;\n const applyTrack = () => {\n track.enabled = !muted && !pushToTalk;\n };\n\n return {\n sessionId: started.sessionId,\n threadId: started.threadId,\n delegate: started.delegate,\n endsAt: started.endsAt,\n mute(on) {\n muted = on;\n applyTrack();\n },\n setPushToTalk(on) {\n pushToTalk = on;\n applyTrack();\n send({\n type: \"session.update\",\n session: {\n type: \"realtime\",\n audio: {\n input: {\n turn_detection: on ? null : (options.turnDetection ?? { type: \"semantic_vad\" }),\n },\n },\n },\n });\n },\n press() {\n if (!pushToTalk || muted) return;\n send({ type: \"input_audio_buffer.clear\" });\n track.enabled = true;\n },\n release() {\n if (!pushToTalk) return;\n track.enabled = false;\n send({ type: \"input_audio_buffer.commit\" });\n send({ type: \"response.create\" });\n },\n async setBody(body) {\n const response = await request(\"PATCH\", sessionUrl, body ?? {});\n if (!response.ok && response.status !== 409) {\n throw new VoiceCallError(`The voice session could not be updated (${response.status}).`, {\n status: response.status,\n });\n }\n },\n async end() {\n try {\n // `keepalive`, so a hang-up sent as the tab closes still reaches the\n // route: minutes are billed until it does.\n await request(\"DELETE\", sessionUrl, undefined, { keepalive: true });\n } finally {\n close();\n }\n },\n close: () => close(),\n };\n}\n","import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport type { RPC } from \"../../client/rpc\";\nimport { applyVoiceEvent, initialVoiceState, voiceStatus, type VoiceState } from \"./state\";\nimport { startVoiceCall, type VoiceCall } from \"./voiceCall\";\nimport type { VoiceCaption, VoiceStatus } from \"./wire\";\n\n/**\n * The voice routes, picked out of `RPC` the way `useChat` picks its agent\n * routes: `this.voice(Controller)` mounted at `\"/chat/voice\"` is the key\n * `\"/chat/voice\"`, carrying the controller's `Body`.\n */\ntype VoiceRoutes = {\n [K in keyof RPC as RPC[K] extends { __voice: true } ? K : never]: RPC[K];\n};\n\ntype BodyOf<P extends keyof VoiceRoutes> = VoiceRoutes[P] extends { body: infer B }\n ? B\n : Record<string, unknown>;\n\nexport type UseVoiceSessionParams<P extends keyof VoiceRoutes> = {\n /** The chat thread delegated turns are stored on, as `useChat` names it. */\n threadId?: string;\n /** The controller's `Body`, sent when the call starts. Change it during the\n * call with `setBody`. */\n body?: BodyOf<P>;\n /** Start with push-to-talk instead of voice activity detection. */\n pushToTalk?: boolean;\n /** Values for the route path's `:params`, e.g. `{ siteId }` for\n * `\"/sites/:siteId/voice\"`. */\n params?: Record<string, string>;\n headers?: Record<string, string>;\n /** The call could not start, or failed. */\n onError?: (error: Error) => void;\n /** The call is over, however it ended. */\n onEnd?: () => void;\n};\n\nexport type UseVoiceSessionResult<P extends keyof VoiceRoutes> = {\n status: VoiceStatus;\n /** The last lines said, the user's and the voice's, oldest first. */\n captions: VoiceCaption[];\n /** Why the call could not start or ended early. `code` is\n * `microphone_denied`, `unsupported`, or the route's error code. */\n error: (Error & { code?: string; status?: number }) | null;\n muted: boolean;\n pushToTalk: boolean;\n /** When the server will end the call, in epoch ms. */\n endsAt?: number;\n sessionId?: string;\n /** Asks for the microphone and starts the call. Call it from a user gesture\n * (a click): browsers allow the microphone and audio playback only then. */\n start(): Promise<void>;\n /** Hangs up. */\n end(): Promise<void>;\n mute(muted?: boolean): void;\n setPushToTalk(on: boolean): void;\n /** Push-to-talk: hold to speak (pointer down / key down). */\n press(): void;\n /** Push-to-talk: release to send (pointer up / key up). */\n release(): void;\n /** Sends a new `body` (the selection, the open page) for the next\n * delegated request. */\n setBody(body: BodyOf<P>): Promise<void>;\n};\n\n/**\n * A voice call to an agent, for a route mounted with `this.voice(Controller)`\n * (#921).\n *\n * ```tsx\n * const voice = useVoiceSession(\"/page-builder/voice\", { threadId, body: { pageId } });\n * <button onClick={() => (voice.status === \"idle\" ? voice.start() : voice.end())}>\n * {voice.status}\n * </button>\n * ```\n *\n * Import it from `gemi/ai/voice`, an entry of its own, so an app that never\n * uses voice never bundles it, and use it from a view or a dynamically imported\n * component, so its code is not in the entry chunk.\n *\n * @experimental Not yet verified against a live call.\n */\nexport function useVoiceSession<P extends keyof VoiceRoutes>(\n path: P,\n params: UseVoiceSessionParams<P> = {},\n): UseVoiceSessionResult<P> {\n // Not `useParams()`, which `useChat` uses: it is in a chunk `gemi/client`\n // shares, and reaching it from here made the app's bundler move it out of\n // the entry chunk into one of its own that every route then imports. A path\n // with parameters names them in `params`.\n const routeParams = params.params;\n const url = useMemo(() => {\n let out = String(path).replace(/^[A-Z]+:/, \"\");\n for (const [key, value] of Object.entries(routeParams ?? {})) {\n out = out.replace(`:${key}?`, value).replace(`:${key}`, value);\n }\n return `/api${out}`;\n }, [path, routeParams && JSON.stringify(routeParams)]);\n\n const [state, setState] = useState<VoiceState>(initialVoiceState);\n const [error, setError] = useState<UseVoiceSessionResult<P>[\"error\"]>(null);\n const [muted, setMuted] = useState(false);\n const [pushToTalk, setPushToTalkState] = useState(params.pushToTalk === true);\n const [call, setCall] = useState<VoiceCall | null>(null);\n const callRef = useRef<VoiceCall | null>(null);\n /** A `start()` in flight: a second click must not start a second call. */\n const startingRef = useRef(false);\n const latest = useRef(params);\n latest.current = params;\n const delegateRef = useRef(\"ask_agent\");\n const mounted = useRef(true);\n\n useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n // Leaving the page ends the call: minutes are billed while it is up.\n void callRef.current?.end().catch(() => undefined);\n callRef.current = null;\n };\n }, []);\n\n const start = useCallback(async () => {\n if (callRef.current || startingRef.current) return;\n startingRef.current = true;\n setError(null);\n setState({ ...initialVoiceState(), phase: \"connecting\" });\n try {\n const { threadId, body, headers } = latest.current;\n const started = await startVoiceCall({\n url,\n threadId,\n body,\n headers,\n pushToTalk,\n onEvent: (event) => {\n if (!mounted.current) return;\n setState((current) =>\n current.phase === \"connecting\"\n ? applyVoiceEvent({ ...current, phase: \"live\" }, event, delegateRef.current)\n : applyVoiceEvent(current, event, delegateRef.current),\n );\n },\n onClose: (closeError) => {\n callRef.current = null;\n if (!mounted.current) return;\n setCall(null);\n setState((current) => ({ ...current, phase: \"ended\", speaking: false, thinking: false }));\n if (closeError) {\n setError(closeError);\n latest.current.onError?.(closeError);\n }\n latest.current.onEnd?.();\n },\n });\n delegateRef.current = started.delegate;\n if (!mounted.current) {\n await started.end().catch(() => undefined);\n return;\n }\n callRef.current = started;\n setCall(started);\n setState((current) =>\n current.phase === \"connecting\" ? { ...current, phase: \"live\" } : current,\n );\n } catch (failure) {\n const reported = failure instanceof Error ? failure : new Error(String(failure));\n if (!mounted.current) return;\n setState((current) => ({ ...current, phase: \"ended\" }));\n setError(reported);\n latest.current.onError?.(reported);\n } finally {\n startingRef.current = false;\n }\n }, [url, pushToTalk]);\n\n const end = useCallback(async () => {\n const current = callRef.current;\n callRef.current = null;\n await current?.end();\n }, []);\n\n const mute = useCallback((on?: boolean) => {\n setMuted((was) => {\n const next = on ?? !was;\n callRef.current?.mute(next);\n return next;\n });\n }, []);\n\n const setPushToTalk = useCallback((on: boolean) => {\n setPushToTalkState(on);\n callRef.current?.setPushToTalk(on);\n }, []);\n\n const press = useCallback(() => {\n callRef.current?.press();\n setState((current) => ({ ...current, userSpeaking: true }));\n }, []);\n\n const release = useCallback(() => {\n callRef.current?.release();\n setState((current) => ({ ...current, userSpeaking: false, thinking: true }));\n }, []);\n\n const setBody = useCallback(async (body: BodyOf<P>) => {\n latest.current = { ...latest.current, body };\n await callRef.current?.setBody(body);\n }, []);\n\n return {\n status: voiceStatus(state),\n captions: state.captions,\n error,\n muted,\n pushToTalk,\n endsAt: call?.endsAt,\n sessionId: call?.sessionId,\n start,\n end,\n mute,\n setPushToTalk,\n press,\n release,\n setBody,\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ import type { VoiceCaption, VoiceStatus } from "./wire";
2
+ /**
3
+ * What `useVoiceSession` shows, as a pure reducer over the call's data-channel
4
+ * events (#921). Separate from the hook so it is tested without a DOM, a
5
+ * microphone or a peer connection.
6
+ *
7
+ * The events are the vendor's, as the data channel delivers them. Which ones
8
+ * arrive there — in particular the `function_call_output` items the server's
9
+ * sideband adds, which end `working` — follows the documented protocol and has
10
+ * not been measured against Azure.
11
+ */
12
+ export type VoiceState = {
13
+ phase: "idle" | "connecting" | "live" | "ended";
14
+ /** The user is speaking (VAD) or holding push-to-talk. */
15
+ userSpeaking: boolean;
16
+ /** The user stopped and no answer has started playing. */
17
+ thinking: boolean;
18
+ /** The voice's audio is playing. */
19
+ speaking: boolean;
20
+ /** Delegate calls with no output yet. */
21
+ working: string[];
22
+ captions: VoiceCaption[];
23
+ };
24
+ export declare const MAX_CAPTIONS = 20;
25
+ export declare function initialVoiceState(): VoiceState;
26
+ export declare function voiceStatus(state: VoiceState): VoiceStatus;
27
+ type Event = {
28
+ type: string;
29
+ [key: string]: any;
30
+ };
31
+ /** One data-channel event onto the state. `delegate` is the delegate tool's name. */
32
+ export declare function applyVoiceEvent(state: VoiceState, event: Event, delegate: string): VoiceState;
33
+ export {};
34
+ //# sourceMappingURL=state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../../ai/voice/state.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAExD;;;;;;;;;GASG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB,KAAK,EAAE,MAAM,GAAG,YAAY,GAAG,MAAM,GAAG,OAAO,CAAC;IAChD,0DAA0D;IAC1D,YAAY,EAAE,OAAO,CAAC;IACtB,0DAA0D;IAC1D,QAAQ,EAAE,OAAO,CAAC;IAClB,oCAAoC;IACpC,QAAQ,EAAE,OAAO,CAAC;IAClB,yCAAyC;IACzC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,EAAE,YAAY,EAAE,CAAC;CAC1B,CAAC;AAEF,eAAO,MAAM,YAAY,KAAK,CAAC;AAE/B,wBAAgB,iBAAiB,IAAI,UAAU,CAS9C;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,UAAU,GAAG,WAAW,CAS1D;AAED,KAAK,KAAK,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CAAE,CAAC;AAElD,qFAAqF;AACrF,wBAAgB,eAAe,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,GAAG,UAAU,CA6C7F"}
@@ -0,0 +1,82 @@
1
+ import type { RPC } from "../../client/rpc";
2
+ import type { VoiceCaption, VoiceStatus } from "./wire";
3
+ /**
4
+ * The voice routes, picked out of `RPC` the way `useChat` picks its agent
5
+ * routes: `this.voice(Controller)` mounted at `"/chat/voice"` is the key
6
+ * `"/chat/voice"`, carrying the controller's `Body`.
7
+ */
8
+ type VoiceRoutes = {
9
+ [K in keyof RPC as RPC[K] extends {
10
+ __voice: true;
11
+ } ? K : never]: RPC[K];
12
+ };
13
+ type BodyOf<P extends keyof VoiceRoutes> = VoiceRoutes[P] extends {
14
+ body: infer B;
15
+ } ? B : Record<string, unknown>;
16
+ export type UseVoiceSessionParams<P extends keyof VoiceRoutes> = {
17
+ /** The chat thread delegated turns are stored on, as `useChat` names it. */
18
+ threadId?: string;
19
+ /** The controller's `Body`, sent when the call starts. Change it during the
20
+ * call with `setBody`. */
21
+ body?: BodyOf<P>;
22
+ /** Start with push-to-talk instead of voice activity detection. */
23
+ pushToTalk?: boolean;
24
+ /** Values for the route path's `:params`, e.g. `{ siteId }` for
25
+ * `"/sites/:siteId/voice"`. */
26
+ params?: Record<string, string>;
27
+ headers?: Record<string, string>;
28
+ /** The call could not start, or failed. */
29
+ onError?: (error: Error) => void;
30
+ /** The call is over, however it ended. */
31
+ onEnd?: () => void;
32
+ };
33
+ export type UseVoiceSessionResult<P extends keyof VoiceRoutes> = {
34
+ status: VoiceStatus;
35
+ /** The last lines said, the user's and the voice's, oldest first. */
36
+ captions: VoiceCaption[];
37
+ /** Why the call could not start or ended early. `code` is
38
+ * `microphone_denied`, `unsupported`, or the route's error code. */
39
+ error: (Error & {
40
+ code?: string;
41
+ status?: number;
42
+ }) | null;
43
+ muted: boolean;
44
+ pushToTalk: boolean;
45
+ /** When the server will end the call, in epoch ms. */
46
+ endsAt?: number;
47
+ sessionId?: string;
48
+ /** Asks for the microphone and starts the call. Call it from a user gesture
49
+ * (a click): browsers allow the microphone and audio playback only then. */
50
+ start(): Promise<void>;
51
+ /** Hangs up. */
52
+ end(): Promise<void>;
53
+ mute(muted?: boolean): void;
54
+ setPushToTalk(on: boolean): void;
55
+ /** Push-to-talk: hold to speak (pointer down / key down). */
56
+ press(): void;
57
+ /** Push-to-talk: release to send (pointer up / key up). */
58
+ release(): void;
59
+ /** Sends a new `body` (the selection, the open page) for the next
60
+ * delegated request. */
61
+ setBody(body: BodyOf<P>): Promise<void>;
62
+ };
63
+ /**
64
+ * A voice call to an agent, for a route mounted with `this.voice(Controller)`
65
+ * (#921).
66
+ *
67
+ * ```tsx
68
+ * const voice = useVoiceSession("/page-builder/voice", { threadId, body: { pageId } });
69
+ * <button onClick={() => (voice.status === "idle" ? voice.start() : voice.end())}>
70
+ * {voice.status}
71
+ * </button>
72
+ * ```
73
+ *
74
+ * Import it from `gemi/ai/voice`, an entry of its own, so an app that never
75
+ * uses voice never bundles it, and use it from a view or a dynamically imported
76
+ * component, so its code is not in the entry chunk.
77
+ *
78
+ * @experimental Not yet verified against a live call.
79
+ */
80
+ export declare function useVoiceSession<P extends keyof VoiceRoutes>(path: P, params?: UseVoiceSessionParams<P>): UseVoiceSessionResult<P>;
81
+ export {};
82
+ //# sourceMappingURL=useVoiceSession.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useVoiceSession.d.ts","sourceRoot":"","sources":["../../../ai/voice/useVoiceSession.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,kBAAkB,CAAC;AAG5C,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAExD;;;;GAIG;AACH,KAAK,WAAW,GAAG;KAChB,CAAC,IAAI,MAAM,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,SAAS;QAAE,OAAO,EAAE,IAAI,CAAA;KAAE,GAAG,CAAC,GAAG,KAAK,GAAG,GAAG,CAAC,CAAC,CAAC;CACzE,CAAC;AAEF,KAAK,MAAM,CAAC,CAAC,SAAS,MAAM,WAAW,IAAI,WAAW,CAAC,CAAC,CAAC,SAAS;IAAE,IAAI,EAAE,MAAM,CAAC,CAAA;CAAE,GAC/E,CAAC,GACD,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE5B,MAAM,MAAM,qBAAqB,CAAC,CAAC,SAAS,MAAM,WAAW,IAAI;IAC/D,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;+BAC2B;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;IACjB,mEAAmE;IACnE,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;oCACgC;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,2CAA2C;IAC3C,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACjC,0CAA0C;IAC1C,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,qBAAqB,CAAC,CAAC,SAAS,MAAM,WAAW,IAAI;IAC/D,MAAM,EAAE,WAAW,CAAC;IACpB,qEAAqE;IACrE,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB;yEACqE;IACrE,KAAK,EAAE,CAAC,KAAK,GAAG;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,GAAG,IAAI,CAAC;IAC3D,KAAK,EAAE,OAAO,CAAC;IACf,UAAU,EAAE,OAAO,CAAC;IACpB,sDAAsD;IACtD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;iFAC6E;IAC7E,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,gBAAgB;IAChB,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACrB,IAAI,CAAC,KAAK,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IAC5B,aAAa,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI,CAAC;IACjC,6DAA6D;IAC7D,KAAK,IAAI,IAAI,CAAC;IACd,2DAA2D;IAC3D,OAAO,IAAI,IAAI,CAAC;IAChB;6BACyB;IACzB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACzC,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,WAAW,EACzD,IAAI,EAAE,CAAC,EACP,MAAM,GAAE,qBAAqB,CAAC,CAAC,CAAM,GACpC,qBAAqB,CAAC,CAAC,CAAC,CA6I1B"}