@runtypelabs/persona 4.26.1 → 4.27.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 (57) hide show
  1. package/dist/approval-ui.d.cts +5 -0
  2. package/dist/approval-ui.d.ts +5 -0
  3. package/dist/artifacts-ui.d.cts +5 -0
  4. package/dist/artifacts-ui.d.ts +5 -0
  5. package/dist/chunk-3255TNY7.js +1 -0
  6. package/dist/{client-history-GOZWBDA5.js → client-history-HTRKHBAF.js} +1 -1
  7. package/dist/client-history.js +1 -1
  8. package/dist/client-stream-N22JHTLE.js +4 -0
  9. package/dist/client-stream.js +3 -3
  10. package/dist/codegen.cjs +1 -1
  11. package/dist/codegen.js +1 -1
  12. package/dist/context-mentions.d.cts +5 -0
  13. package/dist/context-mentions.d.ts +5 -0
  14. package/dist/event-stream-view.d.cts +5 -0
  15. package/dist/event-stream-view.d.ts +5 -0
  16. package/dist/forms-ui.d.cts +7 -0
  17. package/dist/forms-ui.d.ts +7 -0
  18. package/dist/index.cjs +57 -57
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.d.cts +7 -0
  21. package/dist/index.d.ts +7 -0
  22. package/dist/index.global.js +1 -1
  23. package/dist/index.global.js.map +1 -1
  24. package/dist/index.js +58 -58
  25. package/dist/index.js.map +1 -1
  26. package/dist/session-reconnect.d.cts +5 -0
  27. package/dist/session-reconnect.d.ts +5 -0
  28. package/dist/smart-dom-reader.d.cts +5 -0
  29. package/dist/smart-dom-reader.d.ts +5 -0
  30. package/dist/theme-editor-preview.cjs +59 -59
  31. package/dist/theme-editor-preview.d.cts +5 -0
  32. package/dist/theme-editor-preview.d.ts +5 -0
  33. package/dist/theme-editor-preview.js +31 -31
  34. package/dist/theme-editor.d.cts +5 -0
  35. package/dist/theme-editor.d.ts +5 -0
  36. package/dist/voice-runtime-RHRX4F3T.js +17 -0
  37. package/dist/voice-runtime.cjs +8 -8
  38. package/dist/voice-runtime.d.cts +9 -0
  39. package/dist/voice-runtime.d.ts +9 -0
  40. package/dist/voice-runtime.js +8 -8
  41. package/package.json +1 -1
  42. package/src/client-stream.ts +10 -3
  43. package/src/client.test.ts +20 -0
  44. package/src/session.ts +5 -0
  45. package/src/session.voice.test.ts +59 -3
  46. package/src/types.ts +5 -0
  47. package/src/ui.composer-defer-one.test.ts +17 -0
  48. package/src/ui.ts +49 -12
  49. package/src/voice/__fixtures__/voice-wire/server/session_end-provider-error.json +6 -0
  50. package/src/voice/__fixtures__/voice-wire/server/session_end-unknown-reason.json +4 -0
  51. package/src/voice/__fixtures__/voice-wire/server/session_end.json +5 -0
  52. package/src/voice/runtype-voice-provider.ts +112 -10
  53. package/src/voice/voice-delegation.ts +26 -10
  54. package/src/voice/voice.test.ts +176 -0
  55. package/dist/chunk-GKPCIQPB.js +0 -1
  56. package/dist/client-stream-42FOI3EC.js +0 -4
  57. package/dist/voice-runtime-XLU6YK2N.js +0 -17
@@ -1,17 +1,17 @@
1
- var V=class{#e=null;#r=0;#d=[];#i=[];#a=[];#c=!1;#g=!1;#f=0;#n=!1;#m=!1;#l;#t=[];#y=0;#C=!1;#o;#u;#h;#p=null;constructor(e=24e3,t={}){this.#u=e;let s=Math.max(0,t.prebufferMs??0);this.#h=Math.round(e*s/1e3),this.#l=this.#h>0}#b(){if(!this.#e){let t=typeof window<"u"?window:void 0;if(!t)throw new Error("AudioPlaybackManager requires a browser environment");let s=t.AudioContext||t.webkitAudioContext;this.#e=new s({sampleRate:this.#u})}let e=this.#e;return e.state==="suspended"&&!this.#m&&e.resume(),e}enqueue(e){if(e.length===0)return;let t=e;if(this.#p){let n=new Uint8Array(this.#p.length+e.length);n.set(this.#p),n.set(e,this.#p.length),t=n,this.#p=null}if(t.length%2!==0&&(this.#p=new Uint8Array([t[t.length-1]]),t=t.subarray(0,t.length-1)),t.length===0)return;let s=this.#S(t);s.length!==0&&(this.#l?(this.#t.push(s),this.#y+=s.length,clearTimeout(this.#o),this.#y>=this.#h?this.#v():this.#C&&(this.#o=setTimeout(()=>this.#v(),this.#h/this.#u*1e3))):this.#w(s))}markStreamEnd(){this.#t.length>0&&this.#v(),this.#g=!0,this.#k()}flush(){for(let e of this.#d){e.onended=null;try{e.stop(),e.disconnect()}catch{}}this.#d=[],this.#f=0,this.#r=0,this.#c=!1,this.#g=!1,this.#i=[],this.#a=[],this.#p=null,this.#t=[],this.#y=0,this.#l=this.#h>0,this.#n=!1,clearTimeout(this.#o)}setContinuousMode(e){this.#C=e}isPlaying(){return this.#c}onFinished(e){this.#i.push(e)}onStarted(e){this.#a.push(e)}pause(){this.#m=!0,this.#e&&this.#e.state==="running"&&this.#e.suspend()}resume(){this.#m=!1,this.#e&&this.#e.state==="suspended"&&this.#e.resume()}async destroy(){this.flush(),this.#e&&(await this.#e.close(),this.#e=null)}#v(){this.#l=!1;let e=this.#t;this.#t=[],this.#y=0;for(let t of e)this.#w(t)}#w(e){if(e.length===0)return;let t=this.#b(),s=t.createBuffer(1,e.length,this.#u);s.getChannelData(0).set(e);let n=t.createBufferSource();n.buffer=s,n.connect(t.destination);let r=t.currentTime;if(this.#r===0?this.#r=r:this.#r<r&&(this.#r=r,this.#h>0&&(this.#l=!0)),n.start(this.#r),this.#r+=s.duration,this.#d.push(n),this.#f++,this.#c=!0,!this.#n){this.#n=!0;let l=this.#a.slice();this.#a=[];for(let d of l)d()}n.onended=()=>{let l=this.#d.indexOf(n);l!==-1&&(this.#d.splice(l,1),this.#f--,this.#k())}}#k(){if(this.#g&&this.#f<=0&&this.#t.length===0){this.#c=!1,this.#g=!1;let e=this.#i.slice();this.#i=[];for(let t of e)t()}}#S(e){let t=Math.floor(e.length/2),s=new Float32Array(t),n=new DataView(e.buffer,e.byteOffset,e.byteLength);for(let r=0;r<t;r++){let l=n.getInt16(r*2,!0);s[r]=l/32768}return s}};var J="4.26.1";var Y=J;var U=16e3,he=24e3,ge=4096,fe=U*2*8,me=4,ye=1380533830,be=3e4,ve=5e3,we=3e4,Se=3e4,ke=6e5,Ae="full-duplex-v1",Ce=300,Me=2e3,Te=2e3,xe="runtype-browser-v1",Ee=54e4,Pe="You're talking to an AI assistant. Voice is processed by OpenAI.",Ie=8e3,Ve=4e3,_e=12,Z=2e3;function je(i,e){let t="Conversation so far:",s=e.slice(0,Ve).trim(),n=Ie-(s?s.length+2:0)-t.length,r=[];for(let d of i.slice(-_e).reverse()){let a=d.content.replace(/\s+/g," ").trim();if(!a)continue;a.length>Z&&(a=`${a.slice(0,Z-1)}\u2026`);let o=`${d.role==="user"?"User":"Assistant"}: ${a}`;if(o.length+1>n)break;n-=o.length+1,r.unshift(o)}let l=r.length>0?[`${t}
1
+ var P=class{#e=null;#r=0;#d=[];#i=[];#a=[];#c=!1;#f=!1;#m=0;#n=!1;#p=!1;#l;#t=[];#y=0;#M=!1;#o;#u;#g;#h=null;constructor(e=24e3,t={}){this.#u=e;let s=Math.max(0,t.prebufferMs??0);this.#g=Math.round(e*s/1e3),this.#l=this.#g>0}#b(){if(!this.#e){let t=typeof window<"u"?window:void 0;if(!t)throw new Error("AudioPlaybackManager requires a browser environment");let s=t.AudioContext||t.webkitAudioContext;this.#e=new s({sampleRate:this.#u})}let e=this.#e;return e.state==="suspended"&&!this.#p&&e.resume(),e}enqueue(e){if(e.length===0)return;let t=e;if(this.#h){let n=new Uint8Array(this.#h.length+e.length);n.set(this.#h),n.set(e,this.#h.length),t=n,this.#h=null}if(t.length%2!==0&&(this.#h=new Uint8Array([t[t.length-1]]),t=t.subarray(0,t.length-1)),t.length===0)return;let s=this.#S(t);s.length!==0&&(this.#l?(this.#t.push(s),this.#y+=s.length,clearTimeout(this.#o),this.#y>=this.#g?this.#v():this.#M&&(this.#o=setTimeout(()=>this.#v(),this.#g/this.#u*1e3))):this.#w(s))}markStreamEnd(){this.#t.length>0&&this.#v(),this.#f=!0,this.#k()}flush(){for(let e of this.#d){e.onended=null;try{e.stop(),e.disconnect()}catch{}}this.#d=[],this.#m=0,this.#r=0,this.#c=!1,this.#f=!1,this.#i=[],this.#a=[],this.#h=null,this.#t=[],this.#y=0,this.#l=this.#g>0,this.#n=!1,clearTimeout(this.#o)}setContinuousMode(e){this.#M=e}isPlaying(){return this.#c}onFinished(e){this.#i.push(e)}onStarted(e){this.#a.push(e)}pause(){this.#p=!0,this.#e&&this.#e.state==="running"&&this.#e.suspend()}resume(){this.#p=!1,this.#e&&this.#e.state==="suspended"&&this.#e.resume()}async destroy(){this.flush(),this.#e&&(await this.#e.close(),this.#e=null)}#v(){this.#l=!1;let e=this.#t;this.#t=[],this.#y=0;for(let t of e)this.#w(t)}#w(e){if(e.length===0)return;let t=this.#b(),s=t.createBuffer(1,e.length,this.#u);s.getChannelData(0).set(e);let n=t.createBufferSource();n.buffer=s,n.connect(t.destination);let r=t.currentTime;if(this.#r===0?this.#r=r:this.#r<r&&(this.#r=r,this.#g>0&&(this.#l=!0)),n.start(this.#r),this.#r+=s.duration,this.#d.push(n),this.#m++,this.#c=!0,!this.#n){this.#n=!0;let c=this.#a.slice();this.#a=[];for(let d of c)d()}n.onended=()=>{let c=this.#d.indexOf(n);c!==-1&&(this.#d.splice(c,1),this.#m--,this.#k())}}#k(){if(this.#f&&this.#m<=0&&this.#t.length===0){this.#c=!1,this.#f=!1;let e=this.#i.slice();this.#i=[];for(let t of e)t()}}#S(e){let t=Math.floor(e.length/2),s=new Float32Array(t),n=new DataView(e.buffer,e.byteOffset,e.byteLength);for(let r=0;r<t;r++){let c=n.getInt16(r*2,!0);s[r]=c/32768}return s}};var Y="4.27.0";var Z=Y;var W=16e3,fe=24e3,me=4096,ye=W*2*8,be=4,ve=1380533830,we=3e4,Se=1e3,ke=5e3,Ae=3e4,Ce=3e4,Me=6e5,Te="full-duplex-v1",Ee=300,xe=2e3,_e=2e3,Ie="runtype-browser-v1",Pe=54e4,Ve="You're talking to an AI assistant. Voice is processed by OpenAI.",je=new Map([["idle_timeout","Voice call ended after a quiet period."],["max_duration","This voice call reached its time limit."],["provider_ended","The voice session ended."],["quota","Voice is unavailable right now."],["auth_expired","Voice session expired. Tap the mic to reconnect."]]),Q=["provider_error","server_restart"],V="Voice connection lost.",Re=8e3,Le=4e3,Oe=12,ee=2e3;function Ne(i,e){let t="Conversation so far:",s=e.slice(0,Le).trim(),n=Re-(s?s.length+2:0)-t.length,r=[];for(let d of i.slice(-Oe).reverse()){let a=d.content.replace(/\s+/g," ").trim();if(!a)continue;a.length>ee&&(a=`${a.slice(0,ee-1)}\u2026`);let o=`${d.role==="user"?"User":"Assistant"}: ${a}`;if(o.length+1>n)break;n-=o.length+1,r.unshift(o)}let c=r.length>0?[`${t}
2
2
  ${r.join(`
3
- `)}`]:[];return s&&l.push(s),l.join(`
3
+ `)}`]:[];return s&&c.push(s),c.join(`
4
4
 
5
- `)}function Re(i){return i.byteLength>=44&&new DataView(i).getUint32(0,!1)===ye?new Uint8Array(i,44):new Uint8Array(i)}function Q(i){let e=i.replace(/\/+$/,"");return/^wss?:\/\//i.test(e)?e:/^https?:\/\//i.test(e)?e.replace(/^http/i,"ws"):`${typeof window<"u"&&window.location?.protocol==="https:"?"wss:":"ws:"}//${e}`}function Le(i){return Q(i).replace(/^ws/i,"http")}function Oe(i){return typeof i!="number"||!Number.isFinite(i)?we:Math.min(ke,Math.max(Se,Math.round(i)))}var P=class{constructor(e){this.config=e;this.type="runtype";this.#e=null;this.#r=null;this.#d=[];this.#i=null;this.#a=null;this.#c=null;this.#g=[];this.#f=0;this.#n=null;this.#m=!1;this.#l=!1;this.#t=0;this.#y=!1;this.#C=Number.NEGATIVE_INFINITY;this.#o=null;this.#u=!1;this.#h=!1;this.#p=0;this.#b=0;this.#w=!1;this.bridge=null;this.#k=!1;this.#S=new Set;this.#M=new Map;this.#x=new Set;this.#L=!1;this.#E=null;this.#O=null;this.#D=Promise.resolve();this.#P=new Set;this.#T=!1;this.#I=new Set;this.#V=new Set;this.#z=[];this.#N=[];this.#B=[];this.#q=[];this.#U=[]}#e;#r;#d;#i;#a;#c;#g;#f;#n;#m;#l;#t;#y;#C;#o;#u;#h;#p;#b;#v;#w;#k;#S;#M;#x;#R;#L;#E;#O;#D;#P;#T;#I;#V;#z;#N;#B;#q;#U;async connect(){}setSessionBridge(e){this.bridge=e}prewarm(){if(this.#m||this.#o)return;let e=this.config?.agentId,t=this.config?.clientToken,s=this.config?.host;if(!e||!t||!s)return;let n=Date.now();if(!(n-this.#C<be)){this.#C=n;try{this.config?.prewarmMode==="attach"?this.#te(s,e,t):this.#ee(s,e,t)}catch{}}}#ee(e,t,s){if(typeof fetch!="function")return;let n=`${Le(e)}/v1/client/agents/${encodeURIComponent(t)}/voice/prewarm`;fetch(n,{method:"POST",headers:{Authorization:`Bearer ${s}`},keepalive:!0}).catch(()=>{})}#te(e,t,s){let n=new URLSearchParams,r=this.config?.attachIdleMs,l=Oe(r);r!==void 0&&n.set("attachIdleMs",String(l));let d=new WebSocket(this.#G(e,t,n),["runtype.bearer","runtype.attach",s]);d.binaryType="arraybuffer";let a=()=>{},o=new Promise(u=>{a=u}),c={ws:d,state:"connecting",ready:o,settle:a,openedAt:0,idleTimer:void 0};this.#o=c,d.onopen=()=>{this.#o===c&&(c.state=d.protocol==="runtype.attach"?"attached":"live",c.openedAt=Date.now(),this.#X(c,l),c.state==="attached"&&d.send('{"type":"ping"}'),c.settle())},d.onmessage=u=>{if(this.#o!==c||typeof u.data!="string")return;let g;try{g=JSON.parse(u.data)}catch{return}g.type==="attached"&&typeof g.idleMs=="number"&&this.#X(c,g.idleMs-(Date.now()-c.openedAt))},d.onerror=()=>{},d.onclose=()=>{this.#o===c&&this.#_()}}#X(e,t){clearTimeout(e.idleTimer),e.idleTimer=setTimeout(()=>{this.#o===e&&this.#_()},Math.max(0,t))}#_(){let e=this.#o;if(!e)return;this.#o=null,clearTimeout(e.idleTimer);let{ws:t}=e;if(t.onopen=null,t.onmessage=null,t.onerror=null,t.onclose=null,e.settle(),t.readyState===WebSocket.CONNECTING||t.readyState===WebSocket.OPEN)try{t.close(1e3,"prewarm released")}catch{}}async#se(){let e=this.#o;if(!e)return null;if(e.state==="connecting"){let t;await Promise.race([e.ready,new Promise(s=>{t=setTimeout(s,ve)})]),clearTimeout(t)}return this.#o!==e?null:e.state==="connecting"||e.ws.readyState!==WebSocket.OPEN?(this.#_(),null):(this.#o=null,clearTimeout(e.idleTimer),{ws:e.ws,live:e.state==="live"})}#G(e,t,s){let n=["partial_transcript","context"];this.bridge&&this.config?.clientDelegation!==!1&&n.push("client_delegation","delegation_update","delegation_read_back"),s&&n.unshift("attach");let r=new URLSearchParams({voiceProtocol:xe,clientVersion:`persona/${Y}`,voiceCapabilities:Ae,clientCapabilities:n.join(",")});return s?.forEach((l,d)=>r.set(d,l)),`${Q(e)}/ws/agents/${encodeURIComponent(t)}/voice?${r}`}async startListening(){if(this.#m)return;let e=this.config?.agentId,t=this.config?.clientToken,s=this.config?.host;if(!e)throw new Error("Runtype voice requires an agentId");if(!t)throw new Error("Runtype voice requires a clientToken");if(!s)throw new Error("Runtype voice requires a host (or widget apiUrl)");let n=++this.#t;this.#y=!1,this.#m=!0;try{let r=await navigator.mediaDevices.getUserMedia({audio:{sampleRate:U,channelCount:1,echoCancellation:!0}});if(n!==this.#t){r.getTracks().forEach(u=>u.stop());return}this.#i=r;let l=window.AudioContext||window.webkitAudioContext,d=new l({sampleRate:U});d.state==="suspended"&&await d.resume().catch(()=>{}),this.#r=d;let a=this.config?.createPlaybackEngine?await this.config.createPlaybackEngine():new V(he);if(n!==this.#t){a.destroy(),r.getTracks().forEach(u=>u.stop()),d.close().catch(()=>{});return}this.#n=a,this.#F(),this.#ie(d,r,n);let o=await this.#se();if(n!==this.#t){o?.ws.close(1e3,"client ended call");return}if(o){let{ws:u}=o;this.#e=u,this.#K(u,n),o.live||u.send('{"type":"start"}'),this.#W(u),this.#s("listening");return}let c=new WebSocket(this.#G(s,e),["runtype.bearer",t]);c.binaryType="arraybuffer",this.#e=c,c.onopen=()=>{n===this.#t&&(this.#W(c),this.#s("listening"))},this.#K(c,n)}catch(r){throw this.#A(),this.#j(r),this.#s("error"),r}}#K(e,t){e.onmessage=s=>this.#ne(s,t),e.onerror=()=>{t===this.#t&&(this.#j(new Error("Voice connection failed")),this.#s("error"),this.#A())},e.onclose=s=>{if(this.#y){this.#y=!1;return}if(t===this.#t){if(s.code!==1e3){let n=s.code?` (code ${s.code})`:"";this.#j(new Error(`Voice connection closed${n}`)),this.#s("error")}else this.#s("idle");this.#A()}}}getDisclosure(){let e=this.config?.disclosureText;return this.#m&&this.#u&&e!==!1?e||Pe:null}async stopListening(){this.#A(),this.#s("idle")}async disconnect(){this.#A(),this.#s("disconnected"),this.#z=[],this.#N=[],this.#B=[],this.#q=[],this.#U=[]}stopPlayback(){this.#Q();let e=this.#e;e&&e.readyState===WebSocket.OPEN&&(this.#u&&(this.#p=Date.now()+Me,e.send('{"type":"cancel"}')),this.#s("listening"))}getInterruptionMode(){return"barge-in"}isBargeInActive(){return this.#m}onLevel(e){this.#d.push(e)}async deactivateBargeIn(){this.#A(),this.#s("idle")}#ie(e,t,s){let n=e.createMediaStreamSource(t);this.#a=n;let r=e.createScriptProcessor(ge,1,1);this.#c=r,r.onaudioprocess=l=>{if(s!==this.#t)return;let d=this.#e,a=d?.readyState===WebSocket.OPEN;if(d&&!a&&d.readyState!==WebSocket.CONNECTING)return;let o=l.inputBuffer.getChannelData(0),c=new Int16Array(o.length),u=0;for(let g=0;g<o.length;g++){let f=Math.max(-1,Math.min(1,o[g]));u+=f*f,c[g]=f<0?f*32768:f*32767}if(d&&a)this.#W(d),d.send(c.buffer);else for(this.#g.push(c.buffer),this.#f+=c.byteLength;this.#f>fe;)this.#f-=this.#g.shift().byteLength;if(this.#d.length>0){let g=Math.sqrt(u/o.length),f=Math.max(0,Math.min(1,g*me));for(let p of this.#d)p(f)}},n.connect(r),r.connect(e.destination)}#W(e){for(let t of this.#g)e.send(t);this.#g=[],this.#f=0}#ne(e,t){if(t!==this.#t)return;if(e.data instanceof ArrayBuffer){this.#ce(e.data,t);return}let s;try{s=JSON.parse(e.data)}catch{return}switch(s.type){case"session_config":s.speechMode&&(this.#u=s.speechMode==="speech_to_speech",this.#n?.setContinuousMode?.(this.#u)),Array.isArray(s.capabilities)&&(this.#S=new Set(s.capabilities)),typeof s.callId=="string"&&(this.#R=s.callId),this.#k=this.#u&&this.#S.has("client_delegation")&&!!this.bridge,this.#u&&!this.#l&&this.#s("listening"),this.#S.has("context")&&!this.#L&&(this.#L=!0,this.#E=this.#re());break;case"transcript_update":{let n=s.role==="assistant"?"assistant":"user",r=s.utteranceId??s.turnId;if(!r||n==="assistant"&&this.#$())break;let l=String(r);if(n==="user")s.final===!0&&this.#J(t),this.#T=!1;else{if(this.#S.has("delegation_read_back")){let d=s.delegationId;typeof d=="string"&&this.#P.has(d)&&!this.#I.has(l)&&this.#V.add(l)}else this.#T&&!this.#I.has(l)&&(this.#V.add(l),this.#T=!1);if(this.#I.add(l),this.#V.has(l))break}this.#H(n,s.text??"",s.final===!0,{turnId:l,...typeof s.startMs=="number"&&{startMs:s.startMs},...typeof s.endMs=="number"&&{endMs:s.endMs},...this.#k&&{caption:!0}});break}case"delegation_started":case"delegation_completed":{let n=String(s.delegationId??s.turnId);if(s.type==="delegation_started"){this.#J(t);let r=s.input;this.#k&&r&&this.#ae(n,{delegationId:n,userText:String(r.text??""),userUtteranceIds:Array.isArray(r.userUtteranceIds)?r.userUtteranceIds.filter(l=>typeof l=="string"):[],messages:Array.isArray(r.messages)?r.messages:[]},t)}else s.speak!==!1&&this.#P.has(n)&&(this.#T=!0);this.#h=s.type==="delegation_started",!this.#l&&!this.#$()&&this.#s(this.#h?"processing":"listening");break}case"delegation_cancelled":this.#Y(String(s.delegationId),s.reason!=="session_ending"&&s.reason!=="provider_cancelled");break;case"warning":console.warn(`[Persona voice] ${s.code}: ${s.message??""}`,this.#R??""),s.delegationId&&(s.code==="UNKNOWN_DELEGATION"||s.code==="LATE_RESULT_LIMIT")&&this.#Y(String(s.delegationId));break;case"audio_clear":this.#Q(),this.#p=0,this.#s("listening");break;case"transcript_interim":this.#s("listening"),this.#H("user",s.text??"",!1);break;case"transcript_final":{let n=s.role==="assistant"?"assistant":"user";this.#s(n==="user"?"processing":"speaking"),this.#H(n,s.text??"",!0);break}case"audio_end":this.#n?this.#n.markStreamEnd():(this.#l=!1,this.#s("listening"));break;case"metrics":this.#de({llmMs:s.llm_ms,ttsMs:s.tts_ms,firstAudioMs:s.first_audio_ms,totalMs:s.total_ms});break;case"error":this.#j(new Error(s.error||"Voice error")),this.#s("error");break}}async#re(){let e=this.bridge?.getHistory()??[],t="";try{let s=this.config?.callContext;t=String((typeof s=="function"?await s():s)??"")}catch{}return je(e,t)}#J(e){let t=this.#E;if(!t)return;this.#E=null;let s=!1,n=t.then(r=>{let l=this.#e;s||!r||e!==this.#t||l?.readyState!==WebSocket.OPEN||l.send(JSON.stringify({type:"context",text:r}))});this.#O={done:n,drop:()=>s=!0}}async#oe(){let e=this.#O;if(!e)return;let t,s=await Promise.race([e.done.then(()=>!1),new Promise(n=>{t=setTimeout(()=>n(!0),Te)})]);clearTimeout(t),s&&e.drop()}#Y(e,t=!1){this.#x.add(e),this.bridge?.dropDelegation?.(e,t),this.#M.get(e)?.abort()}#ae(e,t,s){let n=this.bridge,r=(l,d,a)=>{let o=this.#e;return s!==this.#t||o?.readyState!==WebSocket.OPEN||this.#x.has(e)?!1:(o.send(JSON.stringify({type:l,delegationId:e,status:d,text:a})),!0)};this.#D=this.#D.then(async()=>{if(s!==this.#t||this.#x.has(e))return;let l=await n.runDelegatedTurn(t).catch(()=>({status:"failed",text:""}));await this.#oe();let d=!!l.followUp,a=d&&this.#S.has("delegation_update");if(!r(a?"delegation_update":"delegation_result",d&&!a?"completed":l.status,l.text)||(l.status!=="failed"&&l.inChat!==!1&&this.#P.add(e),!l.followUp))return;let c=new AbortController;this.#M.set(e,c),l.followUp({signal:c.signal,approvalTimeoutMs:this.config?.approvalTimeoutMs&&Math.min(this.config.approvalTimeoutMs,Ee),readBack:a,onUpdate:u=>r("delegation_update","pending_approval",u)}).then(u=>{this.#M.delete(e),a&&u&&!c.signal.aborted&&r("delegation_result",u.status,u.text)})})}#ce(e,t){if(t!==this.#t||!this.#n||this.#$())return;let s=Re(e);s.length!==0&&(this.#l||(this.#l=!0,this.#s("speaking")),this.#F(),this.#n.enqueue(s),this.#u&&this.#le(s.length))}#$(){return Date.now()<this.#p}#F(){if(this.#w||!this.#n)return;this.#w=!0;let e=this.#t;this.#n.onFinished(()=>{e===this.#t&&(this.#w=!1,this.#Z())})}#le(e){let t=Date.now();this.#b=Math.max(t,this.#b)+e/48,clearTimeout(this.#v),this.#v=setTimeout(()=>this.#Z(),this.#b-t+Ce)}#Z(){this.#l=!1,this.#b=0,this.#e&&this.#e.readyState===WebSocket.OPEN&&this.#s(this.#h?"processing":"listening")}#Q(){this.#n?.flush(),this.#w=!1,this.#F(),this.#l=!1,this.#b=0,clearTimeout(this.#v)}#A(){this.#t+=1,this.#m=!1,this.#l=!1,this.#_(),this.#u=!1,this.#h=!1,this.#k=!1,this.#S=new Set;for(let e of this.#M.values())e.abort();if(this.#M.clear(),this.#x.clear(),this.#R=void 0,this.#L=!1,this.#E=null,this.#O=null,this.#D=Promise.resolve(),this.#P.clear(),this.#T=!1,this.#I.clear(),this.#V.clear(),this.#w=!1,this.#p=0,this.#b=0,clearTimeout(this.#v),this.#g=[],this.#f=0,this.#c&&(this.#c.onaudioprocess=null,this.#c.disconnect(),this.#c=null),this.#a&&(this.#a.disconnect(),this.#a=null),this.#i&&(this.#i.getTracks().forEach(e=>e.stop()),this.#i=null),this.#r&&(this.#r.close().catch(()=>{}),this.#r=null),this.#n&&(this.#n.destroy(),this.#n=null),this.#e){this.#y=!0;try{this.#e.close(1e3,"client ended call")}catch{}this.#e=null}}onResult(e){this.#z.push(e)}onError(e){this.#N.push(e)}onStatusChange(e){this.#B.push(e)}onTranscript(e){this.#q.push(e)}onMetrics(e){this.#U.push(e)}#s(e){this.#B.forEach(t=>t(e))}#j(e){this.#N.forEach(t=>t(e))}#H(e,t,s,n){this.#q.forEach(r=>r(e,t,s,n))}#de(e){this.#U.forEach(t=>t(e))}};var M=class{constructor(e={}){this.config=e;this.type="browser";this.#e=null;this.#r=[];this.#d=[];this.#i=[];this.#a=!1;this.#c=typeof window<"u"?window:void 0}#e;#r;#d;#i;#a;#c;async connect(){this.#i.forEach(e=>e("connected"))}async startListening(){try{if(this.#a)throw new Error("Already listening");if(!this.#c)throw new Error("Window object not available");let e=this.#c.SpeechRecognition||this.#c.webkitSpeechRecognition;if(!e)throw new Error("Browser speech recognition not supported");this.#e=new e,this.#e.lang=this.config?.language||"en-US",this.#e.continuous=this.config?.continuous||!1,this.#e.interimResults=!0,this.#e.onresult=t=>{let s=Array.from(t.results).map(r=>r[0]).map(r=>r.transcript).join(""),n=t.results[t.results.length-1].isFinal;this.#r.forEach(r=>r({text:s,confidence:n?.8:.5,provider:"browser"})),n&&!this.config?.continuous&&this.stopListening()},this.#e.onerror=t=>{this.#d.forEach(s=>s(new Error(t.error))),this.#i.forEach(s=>s("error"))},this.#e.onstart=()=>{this.#a=!0,this.#i.forEach(t=>t("listening"))},this.#e.onend=()=>{this.#a=!1,this.#i.forEach(t=>t("idle"))},this.#e.start()}catch(e){throw this.#d.forEach(t=>t(e)),this.#i.forEach(t=>t("error")),e}}async stopListening(){this.#e&&(this.#e.stop(),this.#e=null),this.#a=!1,this.#i.forEach(e=>e("idle"))}onResult(e){this.#r.push(e)}onError(e){this.#d.push(e)}onStatusChange(e){this.#i.push(e)}async disconnect(){await this.stopListening(),this.#i.forEach(e=>e("disconnected"))}static isSupported(){return"SpeechRecognition"in window||"webkitSpeechRecognition"in window}};function _(i){switch(i.type){case"runtype":if(!i.runtype)throw new Error("Runtype voice provider requires configuration");return new P(i.runtype);case"browser":if(!M.isSupported())throw new Error("Browser speech recognition not supported");return new M(i.browser||{});case"custom":{let e=i.custom;if(!e)throw new Error("Custom voice provider requires a `custom` provider instance or factory");let t=typeof e=="function"?e():e;if(!t||typeof t.startListening!="function")throw new Error("Custom voice provider `custom` must be a VoiceProvider (or a factory returning one)");return t}default:throw new Error(`Unknown voice provider type: ${i.type}`)}}function ee(i){if(i?.type==="custom"&&i.custom)return _({type:"custom",custom:i.custom});if(i?.type==="runtype"&&i.runtype)return _({type:"runtype",runtype:i.runtype});if(M.isSupported())return _({type:"browser",browser:i?.browser||{language:"en-US"}});throw new Error("No supported voice providers available")}function De(i){try{return ee(i),!0}catch{return!1}}var te=i=>i.toLowerCase().replace(/[^\p{L}\p{N}]+/gu,""),W=class{constructor(e){this.host=e;this.turns=new Map;this.latestId=null;this.cancelled=new Set}apply(e,t,s,n,r,l){let d=this.host;t=t.trim();let a=this.turns.get(n);if(a||(a={},this.turns.set(n,a),this.latestId=n),e==="user"){a.userFinal=s;let o=a.userId?d.find(a.userId):void 0;if(o){let c=a.userStartMs===void 0&&r!==void 0;c&&(a.userStartMs=r),d.upsert({...o,content:t,voiceProcessing:!s,...c&&this.placeBefore(this.startedAfter(r))})}else{a.userStartMs=r;let c=a.assistantId?d.find(a.assistantId):void 0;a.userId=d.inject({role:"user",content:t,streaming:!1,voiceProcessing:!s,voiceCaption:l,...this.placeBefore(this.startedAfter(r)??c)}).id}}else if(!this.cancelled.has(n)){a.assistantFinal=s;let o=a.assistantId?d.find(a.assistantId):void 0;if(o){let c=a.assistantStartMs===void 0&&r!==void 0;c&&(a.assistantStartMs=r),d.upsert({...o,content:t,streaming:!s,voiceProcessing:!s,...c&&this.placeBefore(this.startedAfter(r))})}else t&&(a.assistantStartMs=r,a.assistantId=d.inject({role:"assistant",content:t,streaming:!s,voiceProcessing:!s,voiceCaption:l,...this.placeBefore(this.startedAfter(r))}).id);s&&a.assistantId&&d.markSpoken(a.assistantId)}this.sync()}claimUserTurn(e,t=[]){let s=t[t.length-1];for(let r of t)r!==s&&(this.turns.has(r)||this.turns.set(r,{}),this.turns.get(r).claimed=!0);let n;if(s){if(n=this.turns.get(s),n||(n={},this.turns.set(s,n)),n.claimed)return null;(!n.userId||!this.host.find(n.userId))&&(n.userFinal=!0,n.userId=this.host.inject({role:"user",content:e.trim()}).id)}else{let r=te(e),l=[...this.turns.values()].reverse().flatMap(d=>{let a=d.userId&&!d.claimed?this.host.find(d.userId):void 0;return a?[{turn:d,text:te(a.content)}]:[]}).filter(({text:d})=>d&&r);n=(l.find(({text:d})=>d===r)??l.find(({text:d})=>d.startsWith(r)||r.startsWith(d)))?.turn}return n?(n.claimed=!0,n.userId):null}cancel(){let e=this.inFlight();if(e.length===0){let t=this.awaiting();if(!t)return;e=[t]}for(let t of e)this.cancelled.add(t);this.closeReplies(e),this.sync()}fail(e){let t=this.awaiting(),s=this.inFlight();if(!t&&s.length===0)return;for(let l of s)this.turns.get(l).assistantFinal=!0;this.closeReplies(s);let n=this.host.inject({role:"assistant",content:e,streaming:!1,voiceProcessing:!1}),r=t?this.turns.get(t):void 0;r&&(r.assistantId=n.id,r.assistantFinal=!0),this.sync()}settle(){if(this.turns.size===0)return;let e=this.pending(),t=new Set;for(let s of this.turns.values())s.userId&&t.add(s.userId),s.assistantId&&(t.add(s.assistantId),this.host.markSpoken(s.assistantId));this.turns.clear(),this.latestId=null,this.host.settle(t),e&&this.host.setStreaming(!1)}startedAfter(e){if(e===void 0)return;let t;for(let s of this.turns.values())for(let[n,r]of[[s.userId,s.userStartMs],[s.assistantId,s.assistantStartMs]])n&&r!==void 0&&r>e&&(!t||r<t.start)&&(t={start:r,id:n});return t&&this.host.find(t.id)}placeBefore(e){if(!e)return;let t=e.sequence??0,s=t-1;for(let n of this.turns.values())for(let r of[n.userId,n.assistantId]){let l=r?this.host.find(r):void 0,d=l?.sequence;l?.createdAt===e.createdAt&&d!==void 0&&d>s&&d<t&&(s=d)}return{createdAt:e.createdAt,sequence:(s+t)/2}}closeReplies(e){let t=new Set;for(let s of e){let n=this.turns.get(s)?.assistantId;n&&(t.add(n),this.host.markSpoken(n))}t.size>0&&this.host.settle(t)}inFlight(){let e=[];for(let[t,s]of this.turns)s.assistantId&&!s.assistantFinal&&!this.cancelled.has(t)&&e.push(t);return e}awaiting(){let e=this.latestId;if(!e)return null;let t=this.turns.get(e);return t?.userFinal&&!t.assistantId&&!t.claimed&&!this.cancelled.has(e)?e:null}pending(){return this.awaiting()!==null||this.inFlight().length>0}sync(){this.host.setStreaming(this.pending())}};var Ne="I need your answer in the chat before I can continue.";var se="no|nope|never ?mind|decline(?: it| that)?|cancel(?: it| that| this)?(?: (?:the|my|that|this) (?:[a-z]+ )?(?:order|request))?|don'?t (?:do|place) (?:it|that|the (?:[a-z]+ )?order)",Be=new RegExp(`^(?:${se})(?: (?:${se}))?(?: thanks| thank you)?$`),qe=i=>Be.test(i.toLowerCase().replace(/[^a-z' ]+/g," ").replace(/\s+/g," ").trim()),ie="Briefly tell the user what you're about to do and ask them to approve or decline it in the chat. Don't claim it's done.",ne=()=>new Promise(i=>setTimeout(i,50)),$=(i,e)=>i.length>e?`${i.slice(0,e-1).trimEnd()}\u2026`:i,x=i=>i.replace(/^webmcp:/,"").replace(/([a-z\d])([A-Z])/g,"$1 $2").replace(/[_\-.]+/g," ").trim().toLowerCase();function F(i,e=0){if(i==null||i==="")return"";if(typeof i!="object")return String(i);if(e>2)return"";if(Array.isArray(i))return i.map(r=>F(r,e+1)).filter(Boolean).join(", ");let t=i,s=t.quantity??t.qty??t.count,n=t.name??t.item??t.product??t.title;return s!=null&&n!=null&&typeof n!="object"?`${s} ${n}`:Object.entries(t).filter(([r])=>!r.startsWith("_")).map(([r,l])=>{let d=F(l,e+1);return d&&(e?`${x(r)} ${d}`:`${x(r)}: ${d}`)}).filter(Boolean).join(e?", ":"; ")}function re(i){let e=i.length>1?"These actions need the user's approval in the chat before they happen:":"This action needs the user's approval in the chat before it happens:",t=1e3-e.length-ie.length-2,s=i.map(n=>{let r=[x(n.toolName)];n.description&&r.push(`(${$(n.description.trim(),160)})`);let l=F(n.parameters);return l&&r.push(`with ${l}`),n.reason&&r.push(`because: ${$(n.reason.trim(),160)}`),$(`- ${r.join(" ")}`,Math.floor(t/i.length)-1)});return`${e}
5
+ `)}function De(i){return i.byteLength>=44&&new DataView(i).getUint32(0,!1)===ve?new Uint8Array(i,44):new Uint8Array(i)}function te(i){let e=i.replace(/\/+$/,"");return/^wss?:\/\//i.test(e)?e:/^https?:\/\//i.test(e)?e.replace(/^http/i,"ws"):`${typeof window<"u"&&window.location?.protocol==="https:"?"wss:":"ws:"}//${e}`}function qe(i){return te(i).replace(/^ws/i,"http")}function Ue(i){return typeof i!="number"||!Number.isFinite(i)?Ae:Math.min(Me,Math.max(Ce,Math.round(i)))}var _=class{constructor(e){this.config=e;this.type="runtype";this.#e=null;this.#r=null;this.#d=[];this.#i=null;this.#a=null;this.#c=null;this.#f=[];this.#m=0;this.#n=null;this.#p=!1;this.#l=!1;this.#t=0;this.#y=!1;this.#M=Number.NEGATIVE_INFINITY;this.#o=null;this.#u=!1;this.#g=!1;this.#h=0;this.#b=0;this.#w=!1;this.bridge=null;this.#k=!1;this.#S=new Set;this.#T=new Map;this.#_=new Set;this.#I=null;this.#A=null;this.#N=!1;this.#D=!1;this.#q=!1;this.#P=null;this.#U=null;this.#B=Promise.resolve();this.#V=new Set;this.#E=!1;this.#j=new Set;this.#R=new Set;this.#Y=[];this.#W=[];this.#$=[];this.#F=[];this.#H=[]}#e;#r;#d;#i;#a;#c;#f;#m;#n;#p;#l;#t;#y;#M;#o;#u;#g;#h;#b;#v;#w;#k;#S;#T;#_;#O;#I;#A;#J;#N;#D;#q;#P;#U;#B;#V;#E;#j;#R;#Y;#W;#$;#F;#H;async connect(){}setSessionBridge(e){this.bridge=e}prewarm(){if(this.#p||this.#o)return;let e=this.config?.agentId,t=this.config?.clientToken,s=this.config?.host;if(!e||!t||!s)return;let n=Date.now();if(!(n-this.#M<we)){this.#M=n;try{this.config?.prewarmMode==="attach"?this.#oe(s,e,t):this.#re(s,e,t)}catch{}}}#re(e,t,s){if(typeof fetch!="function")return;let n=`${qe(e)}/v1/client/agents/${encodeURIComponent(t)}/voice/prewarm`;fetch(n,{method:"POST",headers:{Authorization:`Bearer ${s}`},keepalive:!0}).catch(()=>{})}#oe(e,t,s){let n=new URLSearchParams,r=this.config?.attachIdleMs,c=Ue(r);r!==void 0&&n.set("attachIdleMs",String(c));let d=new WebSocket(this.#Q(e,t,n),["runtype.bearer","runtype.attach",s]);d.binaryType="arraybuffer";let a=()=>{},o=new Promise(u=>{a=u}),l={ws:d,state:"connecting",ready:o,settle:a,openedAt:0,idleTimer:void 0};this.#o=l,d.onopen=()=>{this.#o===l&&(l.state=d.protocol==="runtype.attach"?"attached":"live",l.openedAt=Date.now(),this.#Z(l,c),l.state==="attached"&&d.send('{"type":"ping"}'),l.settle())},d.onmessage=u=>{if(this.#o!==l||typeof u.data!="string")return;let p;try{p=JSON.parse(u.data)}catch{return}p.type==="attached"&&typeof p.idleMs=="number"&&this.#Z(l,p.idleMs-(Date.now()-l.openedAt))},d.onerror=()=>{},d.onclose=()=>{this.#o===l&&this.#L()}}#Z(e,t){clearTimeout(e.idleTimer),e.idleTimer=setTimeout(()=>{this.#o===e&&this.#L()},Math.max(0,t))}#L(){let e=this.#o;if(!e)return;this.#o=null,clearTimeout(e.idleTimer);let{ws:t}=e;if(t.onopen=null,t.onmessage=null,t.onerror=null,t.onclose=null,e.settle(),t.readyState===WebSocket.CONNECTING||t.readyState===WebSocket.OPEN)try{t.close(1e3,"prewarm released")}catch{}}async#ae(){let e=this.#o;if(!e)return null;if(e.state==="connecting"){let t;await Promise.race([e.ready,new Promise(s=>{t=setTimeout(s,ke)})]),clearTimeout(t)}return this.#o!==e?null:e.state==="connecting"||e.ws.readyState!==WebSocket.OPEN?(this.#L(),null):(this.#o=null,clearTimeout(e.idleTimer),{ws:e.ws,live:e.state==="live"})}#Q(e,t,s){let n=["partial_transcript","context"];this.bridge&&this.config?.clientDelegation!==!1&&n.push("client_delegation","delegation_update","delegation_read_back"),s&&n.unshift("attach");let r=new URLSearchParams({voiceProtocol:Ie,clientVersion:`persona/${Z}`,voiceCapabilities:Te,clientCapabilities:n.join(",")});return s?.forEach((c,d)=>r.set(d,c)),`${te(e)}/ws/agents/${encodeURIComponent(t)}/voice?${r}`}async startListening(){if(this.#p)return;let e=this.#D;e||(this.#N=!1,this.#A=null),this.#D=!1;let t=this.config?.agentId,s=this.config?.clientToken,n=this.config?.host;if(!t)throw new Error("Runtype voice requires an agentId");if(!s)throw new Error("Runtype voice requires a clientToken");if(!n)throw new Error("Runtype voice requires a host (or widget apiUrl)");let r=++this.#t;this.#y=!1,this.#p=!0;try{let c=await navigator.mediaDevices.getUserMedia({audio:{sampleRate:W,channelCount:1,echoCancellation:!0}});if(r!==this.#t){c.getTracks().forEach(p=>p.stop());return}this.#i=c;let d=window.AudioContext||window.webkitAudioContext,a=new d({sampleRate:W});if(this.#r=a,a.state==="suspended"){if(await Promise.race([a.resume().catch(()=>{}),new Promise(p=>setTimeout(p,e?500:Se))]),r!==this.#t)return;if(a.state!=="running")throw e?new Error(this.#A=`${V} Tap the mic to reconnect.`):new Error("Audio blocked until a tap")}let o=this.config?.createPlaybackEngine?await this.config.createPlaybackEngine():new P(fe);if(r!==this.#t){o.destroy(),c.getTracks().forEach(p=>p.stop()),a.close().catch(()=>{});return}this.#n=o,this.#G(),this.#le(a,c,r);let l=await this.#ae();if(r!==this.#t){l?.ws.close(1e3,"client ended call");return}if(l){let{ws:p}=l;this.#e=p,this.#ee(p,r),l.live||p.send('{"type":"start"}'),this.#z(p),this.#s("listening");return}let u=new WebSocket(this.#Q(n,t),["runtype.bearer",s]);u.binaryType="arraybuffer",this.#e=u,u.onopen=()=>{r===this.#t&&(this.#z(u),this.#s("listening"))},this.#ee(u,r)}catch(c){throw this.#C(),this.#x(c),this.#s("error"),c}}#ee(e,t){e.onmessage=s=>this.#de(s,t),e.onerror=()=>{t!==this.#t||this.#I||(this.#x(new Error("Voice connection failed")),this.#s("error"),this.#C())},e.onclose=s=>{if(this.#y){this.#y=!1;return}if(t!==this.#t)return;let n=this.#I??(s.code===4408?{reason:"idle_timeout"}:null);if(!n){if(s.code!==1e3){let r=s.code?` (code ${s.code})`:"";this.#x(new Error(`Voice connection closed${r}`)),this.#s("error")}else this.#s("idle");this.#C();return}this.#ce(n.reason,n.message)}}#ce(e,t){let s=Q.includes(e)&&!this.#N;if(this.#C(),s){this.#N=!0,this.#p=!0,this.#A=e==="provider_error"?`${V} Reconnecting\u2026`:"Reconnecting\u2026",this.#J=setTimeout(()=>{this.#p=!1,this.#D=!0,this.startListening().catch(()=>{})},1e3+Math.random()*1e3),this.#s("idle");return}if(Q.includes(e))return this.#A=V,this.#x(new Error(V)),this.#s("error");if(e==="error")return this.#s("error");this.#A=e==="quota"&&t||je.get(e)||null,this.#s("idle")}takeNotice(){let e=this.#A;return this.#A=null,e}getDisclosure(){let e=this.config?.disclosureText;return this.#p&&this.#u&&e!==!1?e||Ve:null}async stopListening(){this.#C(),this.#s("idle")}async disconnect(){this.#C(),this.#s("disconnected"),this.#Y=[],this.#W=[],this.#$=[],this.#F=[],this.#H=[]}stopPlayback(){this.#ne();let e=this.#e;e&&e.readyState===WebSocket.OPEN&&(this.#u&&(this.#h=Date.now()+xe,e.send('{"type":"cancel"}')),this.#s("listening"))}getInterruptionMode(){return"barge-in"}isBargeInActive(){return this.#p}onLevel(e){this.#d.push(e)}async deactivateBargeIn(){this.#C(),this.#s("idle")}#le(e,t,s){let n=e.createMediaStreamSource(t);this.#a=n;let r=e.createScriptProcessor(me,1,1);this.#c=r,r.onaudioprocess=c=>{if(s!==this.#t)return;let d=this.#e,a=d?.readyState===WebSocket.OPEN;if(d&&!a&&d.readyState!==WebSocket.CONNECTING)return;let o=c.inputBuffer.getChannelData(0),l=new Int16Array(o.length),u=0;for(let p=0;p<o.length;p++){let m=Math.max(-1,Math.min(1,o[p]));u+=m*m,l[p]=m<0?m*32768:m*32767}if(d&&a)this.#z(d),d.send(l.buffer);else for(this.#f.push(l.buffer),this.#m+=l.byteLength;this.#m>ye;)this.#m-=this.#f.shift().byteLength;if(this.#d.length>0){let p=Math.sqrt(u/o.length),m=Math.max(0,Math.min(1,p*be));for(let h of this.#d)h(m)}},n.connect(r),r.connect(e.destination)}#z(e){for(let t of this.#f)e.send(t);this.#f=[],this.#m=0}#de(e,t){if(t!==this.#t)return;if(e.data instanceof ArrayBuffer){this.#ge(e.data,t);return}let s;try{s=JSON.parse(e.data)}catch{return}switch(s.type){case"session_config":s.speechMode&&(this.#u=s.speechMode==="speech_to_speech",this.#n?.setContinuousMode?.(this.#u)),Array.isArray(s.capabilities)&&(this.#S=new Set(s.capabilities)),typeof s.callId=="string"&&(this.#O=s.callId),this.#k=this.#u&&this.#S.has("client_delegation")&&!!this.bridge,this.#u&&!this.#l&&this.#s("listening"),this.#S.has("context")&&!this.#q&&(this.#q=!0,this.#P=this.#ue());break;case"transcript_update":{let n=s.role==="assistant"?"assistant":"user",r=s.utteranceId??s.turnId;if(!r||n==="assistant"&&this.#X())break;let c=String(r);if(n==="user")s.final===!0&&this.#te(t),this.#E=!1;else{if(this.#S.has("delegation_read_back")){let d=s.delegationId;typeof d=="string"&&this.#V.has(d)&&!this.#j.has(c)&&this.#R.add(c)}else this.#E&&!this.#j.has(c)&&(this.#R.add(c),this.#E=!1);if(this.#j.add(c),this.#R.has(c))break}this.#K(n,s.text??"",s.final===!0,{turnId:c,...typeof s.startMs=="number"&&{startMs:s.startMs},...typeof s.endMs=="number"&&{endMs:s.endMs},...this.#k&&{caption:!0}});break}case"delegation_started":case"delegation_completed":{let n=String(s.delegationId??s.turnId);if(s.type==="delegation_started"){this.#te(t);let r=s.input;this.#k&&r&&this.#he(n,{delegationId:n,userText:String(r.text??""),userUtteranceIds:Array.isArray(r.userUtteranceIds)?r.userUtteranceIds.filter(c=>typeof c=="string"):[],messages:Array.isArray(r.messages)?r.messages:[]},t)}else s.speak!==!1&&this.#V.has(n)&&(this.#E=!0);this.#g=s.type==="delegation_started",!this.#l&&!this.#X()&&this.#s(this.#g?"processing":"listening");break}case"delegation_cancelled":this.#se(String(s.delegationId),s.reason!=="session_ending"&&s.reason!=="provider_cancelled");break;case"warning":console.warn(`[Persona voice] ${s.code}: ${s.message??""}`,this.#O??""),s.delegationId&&(s.code==="UNKNOWN_DELEGATION"||s.code==="LATE_RESULT_LIMIT")&&this.#se(String(s.delegationId));break;case"audio_clear":this.#ne(),this.#h=0,this.#s("listening");break;case"transcript_interim":this.#s("listening"),this.#K("user",s.text??"",!1);break;case"transcript_final":{let n=s.role==="assistant"?"assistant":"user";this.#s(n==="user"?"processing":"speaking"),this.#K(n,s.text??"",!0);break}case"audio_end":this.#n?this.#n.markStreamEnd():(this.#l=!1,this.#s("listening"));break;case"metrics":this.#me({llmMs:s.llm_ms,ttsMs:s.tts_ms,firstAudioMs:s.first_audio_ms,totalMs:s.total_ms});break;case"error":this.#x(new Error(s.error||"Voice error")),this.#s("error");break;case"session_end":this.#I={reason:String(s.reason),...typeof s.message=="string"&&{message:s.message}};break}}async#ue(){let e=this.bridge?.getHistory()??[],t="";try{let s=this.config?.callContext;t=String((typeof s=="function"?await s():s)??"")}catch{}return Ne(e,t)}#te(e){let t=this.#P;if(!t)return;this.#P=null;let s=!1,n=t.then(r=>{let c=this.#e;s||!r||e!==this.#t||c?.readyState!==WebSocket.OPEN||c.send(JSON.stringify({type:"context",text:r}))});this.#U={done:n,drop:()=>s=!0}}async#pe(){let e=this.#U;if(!e)return;let t,s=await Promise.race([e.done.then(()=>!1),new Promise(n=>{t=setTimeout(()=>n(!0),_e)})]);clearTimeout(t),s&&e.drop()}#se(e,t=!1){this.#_.add(e),this.bridge?.dropDelegation?.(e,t),this.#T.get(e)?.abort()}#he(e,t,s){let n=this.bridge,r=(c,d,a)=>{let o=this.#e;return s!==this.#t||o?.readyState!==WebSocket.OPEN||this.#_.has(e)?!1:(o.send(JSON.stringify({type:c,delegationId:e,status:d,text:a})),!0)};this.#B=this.#B.then(async()=>{if(s!==this.#t||this.#_.has(e))return;let c=await n.runDelegatedTurn(t).catch(()=>({status:"failed",text:""}));await this.#pe();let d=!!c.followUp,a=d&&this.#S.has("delegation_update");if(!r(a?"delegation_update":"delegation_result",d&&!a?"completed":c.status,c.text)||(c.status!=="failed"&&c.inChat!==!1&&this.#V.add(e),!c.followUp))return;let l=new AbortController;this.#T.set(e,l),c.followUp({signal:l.signal,approvalTimeoutMs:this.config?.approvalTimeoutMs&&Math.min(this.config.approvalTimeoutMs,Pe),readBack:a,onUpdate:u=>r("delegation_update","pending_approval",u)}).then(u=>{this.#T.delete(e),a&&u&&!l.signal.aborted&&r("delegation_result",u.status,u.text)})})}#ge(e,t){if(t!==this.#t||!this.#n||this.#X())return;let s=De(e);s.length!==0&&(this.#l||(this.#l=!0,this.#s("speaking")),this.#G(),this.#n.enqueue(s),this.#u&&this.#fe(s.length))}#X(){return Date.now()<this.#h}#G(){if(this.#w||!this.#n)return;this.#w=!0;let e=this.#t;this.#n.onFinished(()=>{e===this.#t&&(this.#w=!1,this.#ie())})}#fe(e){let t=Date.now();this.#b=Math.max(t,this.#b)+e/48,clearTimeout(this.#v),this.#v=setTimeout(()=>this.#ie(),this.#b-t+Ee)}#ie(){this.#l=!1,this.#b=0,this.#e&&this.#e.readyState===WebSocket.OPEN&&this.#s(this.#g?"processing":"listening")}#ne(){this.#n?.flush(),this.#w=!1,this.#G(),this.#l=!1,this.#b=0,clearTimeout(this.#v)}#C(){this.#t+=1,this.#p=!1,this.#l=!1,this.#L(),this.#u=!1,this.#g=!1,this.#k=!1,this.#S=new Set;for(let e of this.#T.values())e.abort();if(this.#T.clear(),this.#_.clear(),this.#O=void 0,this.#I=null,clearTimeout(this.#J),this.#q=!1,this.#P=null,this.#U=null,this.#B=Promise.resolve(),this.#V.clear(),this.#E=!1,this.#j.clear(),this.#R.clear(),this.#w=!1,this.#h=0,this.#b=0,clearTimeout(this.#v),this.#f=[],this.#m=0,this.#c&&(this.#c.onaudioprocess=null,this.#c.disconnect(),this.#c=null),this.#a&&(this.#a.disconnect(),this.#a=null),this.#i&&(this.#i.getTracks().forEach(e=>e.stop()),this.#i=null),this.#r&&(this.#r.close().catch(()=>{}),this.#r=null),this.#n&&(this.#n.destroy(),this.#n=null),this.#e){this.#y=!0;try{this.#e.close(1e3,"client ended call")}catch{}this.#e=null}}onResult(e){this.#Y.push(e)}onError(e){this.#W.push(e)}onStatusChange(e){this.#$.push(e)}onTranscript(e){this.#F.push(e)}onMetrics(e){this.#H.push(e)}#s(e){this.#$.forEach(t=>t(e))}#x(e){this.#W.forEach(t=>t(e))}#K(e,t,s,n){this.#F.forEach(r=>r(e,t,s,n))}#me(e){this.#H.forEach(t=>t(e))}};var T=class{constructor(e={}){this.config=e;this.type="browser";this.#e=null;this.#r=[];this.#d=[];this.#i=[];this.#a=!1;this.#c=typeof window<"u"?window:void 0}#e;#r;#d;#i;#a;#c;async connect(){this.#i.forEach(e=>e("connected"))}async startListening(){try{if(this.#a)throw new Error("Already listening");if(!this.#c)throw new Error("Window object not available");let e=this.#c.SpeechRecognition||this.#c.webkitSpeechRecognition;if(!e)throw new Error("Browser speech recognition not supported");this.#e=new e,this.#e.lang=this.config?.language||"en-US",this.#e.continuous=this.config?.continuous||!1,this.#e.interimResults=!0,this.#e.onresult=t=>{let s=Array.from(t.results).map(r=>r[0]).map(r=>r.transcript).join(""),n=t.results[t.results.length-1].isFinal;this.#r.forEach(r=>r({text:s,confidence:n?.8:.5,provider:"browser"})),n&&!this.config?.continuous&&this.stopListening()},this.#e.onerror=t=>{this.#d.forEach(s=>s(new Error(t.error))),this.#i.forEach(s=>s("error"))},this.#e.onstart=()=>{this.#a=!0,this.#i.forEach(t=>t("listening"))},this.#e.onend=()=>{this.#a=!1,this.#i.forEach(t=>t("idle"))},this.#e.start()}catch(e){throw this.#d.forEach(t=>t(e)),this.#i.forEach(t=>t("error")),e}}async stopListening(){this.#e&&(this.#e.stop(),this.#e=null),this.#a=!1,this.#i.forEach(e=>e("idle"))}onResult(e){this.#r.push(e)}onError(e){this.#d.push(e)}onStatusChange(e){this.#i.push(e)}async disconnect(){await this.stopListening(),this.#i.forEach(e=>e("disconnected"))}static isSupported(){return"SpeechRecognition"in window||"webkitSpeechRecognition"in window}};function j(i){switch(i.type){case"runtype":if(!i.runtype)throw new Error("Runtype voice provider requires configuration");return new _(i.runtype);case"browser":if(!T.isSupported())throw new Error("Browser speech recognition not supported");return new T(i.browser||{});case"custom":{let e=i.custom;if(!e)throw new Error("Custom voice provider requires a `custom` provider instance or factory");let t=typeof e=="function"?e():e;if(!t||typeof t.startListening!="function")throw new Error("Custom voice provider `custom` must be a VoiceProvider (or a factory returning one)");return t}default:throw new Error(`Unknown voice provider type: ${i.type}`)}}function se(i){if(i?.type==="custom"&&i.custom)return j({type:"custom",custom:i.custom});if(i?.type==="runtype"&&i.runtype)return j({type:"runtype",runtype:i.runtype});if(T.isSupported())return j({type:"browser",browser:i?.browser||{language:"en-US"}});throw new Error("No supported voice providers available")}function Be(i){try{return se(i),!0}catch{return!1}}var ie=i=>i.toLowerCase().replace(/[^\p{L}\p{N}]+/gu,""),$=class{constructor(e){this.host=e;this.turns=new Map;this.latestId=null;this.cancelled=new Set}apply(e,t,s,n,r,c){let d=this.host;t=t.trim();let a=this.turns.get(n);if(a||(a={},this.turns.set(n,a),this.latestId=n),e==="user"){a.userFinal=s;let o=a.userId?d.find(a.userId):void 0;if(o){let l=a.userStartMs===void 0&&r!==void 0;l&&(a.userStartMs=r),d.upsert({...o,content:t,voiceProcessing:!s,...l&&this.placeBefore(this.startedAfter(r))})}else{a.userStartMs=r;let l=a.assistantId?d.find(a.assistantId):void 0;a.userId=d.inject({role:"user",content:t,streaming:!1,voiceProcessing:!s,voiceCaption:c,...this.placeBefore(this.startedAfter(r)??l)}).id}}else if(!this.cancelled.has(n)){a.assistantFinal=s;let o=a.assistantId?d.find(a.assistantId):void 0;if(o){let l=a.assistantStartMs===void 0&&r!==void 0;l&&(a.assistantStartMs=r),d.upsert({...o,content:t,streaming:!s,voiceProcessing:!s,...l&&this.placeBefore(this.startedAfter(r))})}else t&&(a.assistantStartMs=r,a.assistantId=d.inject({role:"assistant",content:t,streaming:!s,voiceProcessing:!s,voiceCaption:c,...this.placeBefore(this.startedAfter(r))}).id);s&&a.assistantId&&d.markSpoken(a.assistantId)}this.sync()}claimUserTurn(e,t=[]){let s=t[t.length-1];for(let r of t)r!==s&&(this.turns.has(r)||this.turns.set(r,{}),this.turns.get(r).claimed=!0);let n;if(s){if(n=this.turns.get(s),n||(n={},this.turns.set(s,n)),n.claimed)return null;(!n.userId||!this.host.find(n.userId))&&(n.userFinal=!0,n.userId=this.host.inject({role:"user",content:e.trim()}).id)}else{let r=ie(e),c=[...this.turns.values()].reverse().flatMap(d=>{let a=d.userId&&!d.claimed?this.host.find(d.userId):void 0;return a?[{turn:d,text:ie(a.content)}]:[]}).filter(({text:d})=>d&&r);n=(c.find(({text:d})=>d===r)??c.find(({text:d})=>d.startsWith(r)||r.startsWith(d)))?.turn}return n?(n.claimed=!0,n.userId):null}cancel(){let e=this.inFlight();if(e.length===0){let t=this.awaiting();if(!t)return;e=[t]}for(let t of e)this.cancelled.add(t);this.closeReplies(e),this.sync()}fail(e){let t=this.awaiting(),s=this.inFlight();if(!t&&s.length===0)return;for(let c of s)this.turns.get(c).assistantFinal=!0;this.closeReplies(s);let n=this.host.inject({role:"assistant",content:e,streaming:!1,voiceProcessing:!1}),r=t?this.turns.get(t):void 0;r&&(r.assistantId=n.id,r.assistantFinal=!0),this.sync()}settle(){if(this.turns.size===0)return;let e=this.pending(),t=new Set;for(let s of this.turns.values())s.userId&&t.add(s.userId),s.assistantId&&(t.add(s.assistantId),this.host.markSpoken(s.assistantId));this.turns.clear(),this.latestId=null,this.host.settle(t),e&&this.host.setStreaming(!1)}startedAfter(e){if(e===void 0)return;let t;for(let s of this.turns.values())for(let[n,r]of[[s.userId,s.userStartMs],[s.assistantId,s.assistantStartMs]])n&&r!==void 0&&r>e&&(!t||r<t.start)&&(t={start:r,id:n});return t&&this.host.find(t.id)}placeBefore(e){if(!e)return;let t=e.sequence??0,s=t-1;for(let n of this.turns.values())for(let r of[n.userId,n.assistantId]){let c=r?this.host.find(r):void 0,d=c?.sequence;c?.createdAt===e.createdAt&&d!==void 0&&d>s&&d<t&&(s=d)}return{createdAt:e.createdAt,sequence:(s+t)/2}}closeReplies(e){let t=new Set;for(let s of e){let n=this.turns.get(s)?.assistantId;n&&(t.add(n),this.host.markSpoken(n))}t.size>0&&this.host.settle(t)}inFlight(){let e=[];for(let[t,s]of this.turns)s.assistantId&&!s.assistantFinal&&!this.cancelled.has(t)&&e.push(t);return e}awaiting(){let e=this.latestId;if(!e)return null;let t=this.turns.get(e);return t?.userFinal&&!t.assistantId&&!t.claimed&&!this.cancelled.has(e)?e:null}pending(){return this.awaiting()!==null||this.inFlight().length>0}sync(){this.host.setStreaming(this.pending())}};var We="I need your answer in the chat before I can continue.";var ne="no|nope|never ?mind|decline(?: it| that)?|cancel(?: it| that| this)?(?: (?:the|my|that|this) (?:[a-z]+ )?(?:order|request))?|don'?t (?:do|place) (?:it|that|the (?:[a-z]+ )?order)",$e=new RegExp(`^(?:${ne})(?: (?:${ne}))?(?: thanks| thank you)?$`),Fe=i=>$e.test(i.toLowerCase().replace(/[^a-z' ]+/g," ").replace(/\s+/g," ").trim()),re="Briefly tell the user what you're about to do and ask them to approve or decline it in the chat. Don't claim it's done.",oe=()=>new Promise(i=>setTimeout(i,50)),F=(i,e)=>i.length>e?`${i.slice(0,e-1).trimEnd()}\u2026`:i,x=i=>i.replace(/^webmcp:/,"").replace(/([a-z\d])([A-Z])/g,"$1 $2").replace(/[_\-.]+/g," ").trim().toLowerCase();function H(i,e=0){if(i==null||i==="")return"";if(typeof i!="object")return String(i);if(e>2)return"";if(Array.isArray(i))return i.map(r=>H(r,e+1)).filter(Boolean).join(", ");let t=i,s=t.quantity??t.qty??t.count,n=t.name??t.item??t.product??t.title;return s!=null&&n!=null&&typeof n!="object"?`${s} ${n}`:Object.entries(t).filter(([r])=>!r.startsWith("_")).map(([r,c])=>{let d=H(c,e+1);return d&&(e?`${x(r)} ${d}`:`${x(r)}: ${d}`)}).filter(Boolean).join(e?", ":"; ")}function ae(i){let e=i.length>1?"These actions need the user's approval in the chat before they happen:":"This action needs the user's approval in the chat before it happens:",t=1e3-e.length-re.length-2,s=i.map(n=>{let r=[x(n.toolName)];n.description&&r.push(`(${F(n.description.trim(),160)})`);let c=H(n.parameters);return c&&r.push(`with ${c}`),n.reason&&r.push(`because: ${F(n.reason.trim(),160)}`),F(`- ${r.join(" ")}`,Math.floor(t/i.length)-1)});return`${e}
6
6
  ${s.join(`
7
7
  `)}
8
8
 
9
- ${ie}`}var j=i=>i.variant==="approval"&&i.approval?.status==="pending";function Ue(i){let e=null,t=null,s=()=>i.track(e??t),n=[],r=new Map,l=new Map,d=(p,m)=>{r.set(p,[...r.get(p)??[],m]),r.size>32&&r.delete(r.keys().next().value)},a=p=>i.messages().find(m=>m.id===p),o=p=>p.approvals.filter(m=>a(m)?.approval?.status==="pending"),c=p=>x(a(p)?.approval?.toolName??""),u=(p,m,v)=>{p.outcome={status:m,text:v};for(let w of o(p))i.decide(w)},g=p=>p.flatMap(m=>i.messages().find(v=>v.id===m)??[]).filter(m=>!m.variant&&m.content.trim()).map(m=>m.content.trim()).join(`
9
+ ${re}`}var R=i=>i.variant==="approval"&&i.approval?.status==="pending";function He(i){let e=null,t=null,s=()=>i.track(e??t),n=[],r=new Map,c=new Map,d=(h,f)=>{r.set(h,[...r.get(h)??[],f]),r.size>32&&r.delete(r.keys().next().value)},a=h=>i.messages().find(f=>f.id===h),o=h=>h.approvals.filter(f=>a(f)?.approval?.status==="pending"),l=h=>x(a(h)?.approval?.toolName??""),u=(h,f,w)=>{h.outcome={status:f,text:w};for(let y of o(h))i.decide(y)},p=h=>h.flatMap(f=>i.messages().find(w=>w.id===f)??[]).filter(f=>!f.variant&&f.content.trim()).map(f=>f.content.trim()).join(`
10
10
 
11
- `),f=async p=>{do await ne();while(i.busy()&&!(p&&(i.parked()||p.failed)))};return{getHistory:()=>i.messages().flatMap(p=>(p.role==="user"||p.role==="assistant")&&!p.variant&&!p.voiceProcessing&&!p.voiceCaption&&p.content?[{role:p.role,content:p.content}]:[]),dropDelegation(p,m){l.set(p,!!m),l.size>32&&l.delete(l.keys().next().value);let v=m&&n.find(w=>w.delegationId===p);v&&!v.outcome&&u(v,"timeout","");for(let w of r.get(p)??[])w.dropped=!0,w.ids.length&&i.unspoken(w.ids)},async runDelegatedTurn({delegationId:p,userText:m,userUtteranceIds:v}){let w=n.filter(h=>o(h).length),E=w.flatMap(o);if(E.length===1&&qe(m)){let h=c(E[0]);return u(w[0],"denied",""),{status:"denied",text:`Okay, I cancelled the ${h} request. Nothing was done.`,inChat:!1}}let H=i.parked()&&i.messages().every(h=>!j(h)||E.includes(h.id));i.busy()&&!H&&await f();let oe=new Set(i.messages().map(h=>h.id)),R=()=>i.messages().filter(h=>!oe.has(h.id)&&h.variant==="approval"),k={ids:[],failed:!1};d(p,k),e=k,s();try{await i.send(m,i.claim(m,v)??void 0),await f(k)}catch{k.failed=!0}finally{e=null,s()}let ae=k.ids.flatMap(h=>i.messages().find(b=>b.id===h)??[]),T=g(k.ids),I=k.failed?[]:R().filter(j),ce=new Set(I.map(h=>x(h.approval.toolName)));for(let h of w){if(h.outcome)continue;for(let S of o(h).filter(L=>ce.has(c(L))))h.replaced.add(S),i.decide(S);let b=c(h.approvals[0]);h.approvals.every(S=>h.replaced.has(S))?h.outcome={status:"cancelled",text:`The earlier ${b} request was replaced by the new one; it was not done.`}:H&&h.approvals.every(S=>a(S)?.approval?.status==="denied")&&(h.outcome={status:"cancelled",text:`The earlier ${b} request was replaced by the new one; it was not done.`})}if(I.length){let h=new Set(I.map(S=>S.id)),b={delegationId:p,approvals:[...h],at:Date.now(),replaced:new Set};return n.push(b),l.get(p)&&u(b,"timeout",""),T=`${T}
11
+ `),m=async h=>{do await oe();while(i.busy()&&!(h&&(i.parked()||h.failed)))};return{getHistory:()=>{let h=i.messages().filter(y=>(y.role==="user"||y.role==="assistant")&&!y.variant&&!y.voiceProcessing&&!y.streaming&&y.content),f=!1,w=[];for(let y=h.length-1;y>=0;y-=1){let k=h[y];if(k.role==="user")f=!1;else if(!k.voiceCaption)f=!0;else if(f)continue;w.unshift({role:k.role,content:k.content})}return w},dropDelegation(h,f){c.set(h,!!f),c.size>32&&c.delete(c.keys().next().value);let w=f&&n.find(y=>y.delegationId===h);w&&!w.outcome&&u(w,"timeout","");for(let y of r.get(h)??[])y.dropped=!0,y.ids.length&&i.unspoken(y.ids)},async runDelegatedTurn({delegationId:h,userText:f,userUtteranceIds:w}){let y=n.filter(g=>o(g).length),k=y.flatMap(o);if(k.length===1&&Fe(f)){let g=l(k[0]);return u(y[0],"denied",""),{status:"denied",text:`Okay, I cancelled the ${g} request. Nothing was done.`,inChat:!1}}let z=i.parked()&&i.messages().every(g=>!R(g)||k.includes(g.id));i.busy()&&!z&&await m();let ce=new Set(i.messages().map(g=>g.id)),L=()=>i.messages().filter(g=>!ce.has(g.id)&&g.variant==="approval"),A={ids:[],failed:!1};d(h,A),e=A,s();try{await i.send(f,i.claim(f,w)??void 0),await m(A)}catch{A.failed=!0}finally{e=null,s()}let le=A.ids.flatMap(g=>i.messages().find(v=>v.id===g)??[]),E=p(A.ids),I=A.failed?[]:L().filter(R),de=new Set(I.map(g=>x(g.approval.toolName)));for(let g of y){if(g.outcome)continue;for(let S of o(g).filter(O=>de.has(l(O))))g.replaced.add(S),i.decide(S);let v=l(g.approvals[0]);g.approvals.every(S=>g.replaced.has(S))?g.outcome={status:"cancelled",text:`The earlier ${v} request was replaced by the new one; it was not done.`}:z&&g.approvals.every(S=>a(S)?.approval?.status==="denied")&&(g.outcome={status:"cancelled",text:`The earlier ${v} request was replaced by the new one; it was not done.`})}if(I.length){let g=new Set(I.map(S=>S.id)),v={delegationId:h,approvals:[...g],at:Date.now(),replaced:new Set};return n.push(v),c.get(h)&&u(v,"timeout",""),E=`${E}
12
12
 
13
- ${re(I.map(S=>S.approval))}`.trim(),{status:"pending_approval",text:T,followUp:async({signal:S,approvalTimeoutMs:L=3e5,readBack:z=!0,onUpdate:le})=>{let A={ids:[],failed:!1};r.delete(p),d(p,A);let X=()=>R().filter(y=>h.has(y.id)||A.ids.includes(y.id)),de=()=>R().filter(y=>j(y)&&!h.has(y.id)&&!n.some(C=>C.approvals.includes(y.id))&&(A.ids.includes(y.id)||y.approval?.toolType==="webmcp"&&t===A)),G=()=>{t=A,s()};z&&G();try{for(;!S.aborted&&!b.outcome&&(e||i.busy()||X().some(j));){z&&t!==A&&!o(b).length&&G();let y=de();if(y.length){for(let C of y)h.add(C.id),b.approvals.push(C.id);b.at=Date.now(),le?.(re(y.map(C=>C.approval)))}if(Date.now()-b.at>=L&&o(b).length&&(!i.busy()||i.parked())){u(b,"timeout",`That ${c(b.approvals[0])} request expired, so nothing was done.`);break}await ne()}}finally{t===A&&(t=null,s()),n.splice(n.indexOf(b),1)}if(S.aborted)return null;if(b.outcome)return b.outcome;if(A.failed)return{status:"failed",text:`The ${c(b.approvals[0])} request didn't complete.`};let K=X(),O=y=>K.filter(y).map(C=>x(C.approval.toolName)).join(", "),D=K.some(y=>y.approval?.status==="approved"),N=O(y=>y.approval?.status==="denied"&&!b.replaced.has(y.id)),B=O(y=>b.replaced.has(y.id)),q=O(y=>y.approval?.status==="timeout"),ue=[D?g(A.ids):"",N&&`The user declined the ${N} request in the chat, so ${D?"that part was not done":"nothing was done"}.`,B&&`The earlier ${B} request was replaced by the new one; it was not done.`,q&&`The ${q} request timed out, so it was not done.`].filter(Boolean).join(`
13
+ ${ae(I.map(S=>S.approval))}`.trim(),{status:"pending_approval",text:E,followUp:async({signal:S,approvalTimeoutMs:O=3e5,readBack:X=!0,onUpdate:ue})=>{let C={ids:[],failed:!1};r.delete(h),d(h,C);let G=()=>L().filter(b=>g.has(b.id)||C.ids.includes(b.id)),pe=()=>L().filter(b=>R(b)&&!g.has(b.id)&&!n.some(M=>M.approvals.includes(b.id))&&(C.ids.includes(b.id)||b.approval?.toolType==="webmcp"&&t===C)),K=()=>{t=C,s()};X&&K();try{for(;!S.aborted&&!v.outcome&&(e||i.busy()||G().some(R));){X&&t!==C&&!o(v).length&&K();let b=pe();if(b.length){for(let M of b)g.add(M.id),v.approvals.push(M.id);v.at=Date.now(),ue?.(ae(b.map(M=>M.approval)))}if(Date.now()-v.at>=O&&o(v).length&&(!i.busy()||i.parked())){u(v,"timeout",`That ${l(v.approvals[0])} request expired, so nothing was done.`);break}await oe()}}finally{t===C&&(t=null,s()),n.splice(n.indexOf(v),1)}if(S.aborted)return null;if(v.outcome)return v.outcome;if(C.failed)return{status:"failed",text:`The ${l(v.approvals[0])} request didn't complete.`};let J=G(),N=b=>J.filter(b).map(M=>x(M.approval.toolName)).join(", "),D=J.some(b=>b.approval?.status==="approved"),q=N(b=>b.approval?.status==="denied"&&!v.replaced.has(b.id)),U=N(b=>v.replaced.has(b.id)),B=N(b=>b.approval?.status==="timeout"),he=[D?p(C.ids):"",q&&`The user declined the ${q} request in the chat, so ${D?"that part was not done":"nothing was done"}.`,U&&`The earlier ${U} request was replaced by the new one; it was not done.`,B&&`The ${B} request timed out, so it was not done.`].filter(Boolean).join(`
14
14
 
15
- `);return{status:D?"completed":N?"denied":B?"cancelled":q?"timeout":"completed",text:ue}}}}return!k.failed&&i.parked()||ae.some(h=>h.agentMetadata?.awaitingLocalTool&&!h.agentMetadata.askUserQuestionAnswered&&!h.toolCall?.name?.startsWith("webmcp:"))?{status:k.failed?"failed":"completed",text:`${T}
15
+ `);return{status:D?"completed":q?"denied":U?"cancelled":B?"timeout":"completed",text:he}}}}return!A.failed&&i.parked()||le.some(g=>g.agentMetadata?.awaitingLocalTool&&!g.agentMetadata.askUserQuestionAnswered&&!g.toolCall?.name?.startsWith("webmcp:"))?{status:A.failed?"failed":"completed",text:`${E}
16
16
 
17
- ${Ne}`.trim()}:{status:!k.failed&&T?"completed":"failed",text:T}}}}var We=(i,e,t,s)=>{let n=i.session;try{i.keyed=t?new t({find:o=>n.messages.find(c=>c.id===o),inject:o=>n.injectMessage(o),upsert:o=>n.upsertMessage(o),settle:o=>{n.messages=n.messages.map(c=>o.has(c.id)?{...c,streaming:!1,voiceProcessing:!1}:c),n.callbacks.onMessagesChanged([...n.messages])},setStreaming:o=>{(o||!i.busy())&&i.setStreaming(o)},markSpoken:o=>{n.ttsSpokenMessageIds.add(o)}}):null,s&&e.setSessionBridge&&e.setSessionBridge(s({messages:()=>n.messages,busy:()=>i.busy(),parked:()=>n.webMcpApprovalResolvers.size>0,claim:(o,c)=>i.keyed?.claimUserTurn(o,c)??null,send:(o,c)=>n.sendMessage(o,{viaVoice:!0,voiceTurn:{userMessageId:c}}),track:o=>{i.setDelegation(o)},unspoken:o=>{for(let c of o)n.ttsSpokenMessageIds.delete(c);n.streaming||n.speakLatestAssistantMessage(o)},decide:o=>{if(n.webMcpApprovalResolvers.has(o))return n.resolveWebMcpApproval(o,"denied");let c=n.messages.find(u=>u.id===o)?.approval;c?.status==="pending"&&n.resolveApproval(c,"denied")}}));let r=i.generation(),l=()=>i.provider()===e&&r===i.generation(),a=(n.config.voiceRecognition??{}).processingErrorText??"Voice processing failed. Please try again.";e.onResult(o=>{l()&&o.provider!=="runtype"&&o.text&&o.text.trim()&&n.sendMessage(o.text,{viaVoice:!0})}),e.onTranscript&&e.onTranscript((o,c,u,g)=>{if(l()){if(g?.turnId){i.keyed?.apply(o,c,u,g.turnId,g.startMs,g.caption);return}if(o==="user"){if(i.pendingUser)n.upsertMessage({id:i.pendingUser,role:"user",content:c,createdAt:new Date().toISOString(),streaming:!1,voiceProcessing:!u});else{let f=n.injectMessage({role:"user",content:c,streaming:!1,voiceProcessing:!u});i.pendingUser=f.id}if(u){i.turns().start(g?.turnId),i.pendingUser=null;let f=n.injectMessage({role:"assistant",content:"",streaming:!0,voiceProcessing:!0});i.pendingAssistant=f.id,i.setStreaming(!0)}}else{if(!i.turns().accepts(g?.turnId))return;if(i.pendingAssistant)n.upsertMessage({id:i.pendingAssistant,role:"assistant",content:c,createdAt:new Date().toISOString(),streaming:!u,voiceProcessing:!u});else{let f=n.injectMessage({role:"assistant",content:c,streaming:!u,voiceProcessing:!u});i.pendingAssistant=f.id}u&&(i.pendingAssistant&&n.ttsSpokenMessageIds.add(i.pendingAssistant),i.setStreaming(!1),i.pendingAssistant=null)}}}),e.onLevel&&e.onLevel(o=>{l()&&i.setLevel(Number.isFinite(o)?Math.max(0,Math.min(1,o)):0)}),e.onMetrics&&e.onMetrics(o=>{l()&&n.config.voiceRecognition?.onMetrics?.(o)}),e.onError(o=>{l()&&(console.error("Voice error:",o),i.pendingAssistant&&(n.upsertMessage({id:i.pendingAssistant,role:"assistant",content:a,createdAt:new Date().toISOString(),streaming:!1,voiceProcessing:!1}),i.setStreaming(!1),i.pendingUser=null,i.pendingAssistant=null),i.keyed?.fail(a))}),e.onStatusChange(o=>{l()&&(i.setStatus(o),(o==="listening"||o==="idle"||o==="disconnected")&&i.settlePending(o!=="listening"),(o==="idle"||o==="disconnected")&&i.keyed?.settle(),n.callbacks.onVoiceStatusChanged?.(o))}),e.connect()}catch(r){console.error("Failed to setup voice:",r)}};var $e=i=>{let e=a=>{if(i.original)return;let o=i.config().voiceRecognition??{};i.original={backgroundColor:a.style.backgroundColor,color:a.style.color,borderColor:a.style.borderColor,iconName:o.iconName??"mic",iconSize:parseFloat(o.iconSize??"")||i.iconFallbackPx}},t=(a,o,c)=>{let u=a.querySelector("svg");u&&u.remove();let g=i.original?.iconSize??(parseFloat(i.config().voiceRecognition?.iconSize??"")||i.iconFallbackPx),f=i.renderIcon(o,g,c,1.5);f&&a.appendChild(f)},s=a=>{a.classList.remove("persona-voice-recording","persona-voice-processing","persona-voice-speaking")};return{recording:()=>{let a=i.mic();if(!a)return;e(a);let o=i.config().voiceRecognition??{},c=o.recordingBackgroundColor,u=o.recordingIconColor,g=o.recordingBorderColor;if(s(a),a.classList.add("persona-voice-recording"),i.setMicState("recording"),a.style.backgroundColor=c??"var(--persona-voice-recording-bg, #ef4444)",a.style.color=u??"var(--persona-voice-recording-indicator, #ffffff)",u){let f=a.querySelector("svg");f&&f.setAttribute("stroke",u)}g&&(a.style.borderColor=g),a.setAttribute("aria-label","Stop voice recognition")},processing:()=>{let a=i.mic();if(!a)return;e(a);let o=i.config().voiceRecognition??{},c=i.session().getVoiceInterruptionMode(),u=i.original,g=o.processingIconName??"loader",f=o.processingIconColor??u?.color??"",p=o.processingBackgroundColor??u?.backgroundColor??"",m=o.processingBorderColor??u?.borderColor??"";s(a),a.classList.add("persona-voice-processing"),i.setMicState("processing"),a.style.backgroundColor=p,a.style.borderColor=m;let v=f||"currentColor";a.style.color=v,t(a,g,v),a.setAttribute("aria-label","Processing voice input"),c==="none"&&(a.style.cursor="default")},speaking:()=>{let a=i.mic();if(!a)return;e(a);let o=i.config().voiceRecognition??{},c=i.session().getVoiceInterruptionMode(),u=i.original,g=c==="cancel"?"square":c==="barge-in"?"mic":"volume-2",f=o.speakingIconName??g,p=o.speakingIconColor??(c==="barge-in"?o.recordingIconColor??u?.color??"":u?.color??""),m=o.speakingBackgroundColor??(c==="barge-in"?o.recordingBackgroundColor??"var(--persona-voice-recording-bg, #ef4444)":u?.backgroundColor??""),v=o.speakingBorderColor??(c==="barge-in"?o.recordingBorderColor??"":u?.borderColor??"");s(a),a.classList.add("persona-voice-speaking"),i.setMicState("speaking"),a.style.backgroundColor=m,a.style.borderColor=v;let w=p||"currentColor";a.style.color=w,t(a,f,w);let E=c==="cancel"?"Stop playback and re-record":c==="barge-in"?"Speak to interrupt":"Agent is speaking";a.setAttribute("aria-label",E),c==="none"&&(a.style.cursor="default"),c==="barge-in"&&a.classList.add("persona-voice-recording")},reset:()=>{let a=i.mic();if(!a)return;s(a),i.setMicState("idle");let o=i.original;o&&(a.style.backgroundColor=o.backgroundColor??"",a.style.color=o.color??"",a.style.borderColor=o.borderColor??"",t(a,o.iconName,o.color||"currentColor"),i.original=null),a.style.cursor="",a.setAttribute("aria-label","Start voice recognition")}}};export{M as BrowserVoiceProvider,W as KeyedVoiceTranscript,P as RuntypeVoiceProvider,ee as createBestAvailableVoiceProvider,$e as createMicStateStyles,_ as createVoiceProvider,Ue as createVoiceSessionBridge,De as isVoiceSupported,We as wireSessionVoice};
17
+ ${We}`.trim()}:{status:!A.failed&&E?"completed":"failed",text:E}}}}var ze=(i,e,t,s)=>{let n=i.session;try{i.keyed=t?new t({find:o=>n.messages.find(l=>l.id===o),inject:o=>n.injectMessage(o),upsert:o=>n.upsertMessage(o),settle:o=>{n.messages=n.messages.map(l=>o.has(l.id)?{...l,streaming:!1,voiceProcessing:!1}:l),n.callbacks.onMessagesChanged([...n.messages])},setStreaming:o=>{(o||!i.busy())&&i.setStreaming(o)},markSpoken:o=>{n.ttsSpokenMessageIds.add(o)}}):null,s&&e.setSessionBridge&&e.setSessionBridge(s({messages:()=>n.messages,busy:()=>i.busy(),parked:()=>n.webMcpApprovalResolvers.size>0,claim:(o,l)=>i.keyed?.claimUserTurn(o,l)??null,send:(o,l)=>n.sendMessage(o,{viaVoice:!0,voiceTurn:{userMessageId:l}}),track:o=>{i.setDelegation(o)},unspoken:o=>{for(let l of o)n.ttsSpokenMessageIds.delete(l);n.streaming||n.speakLatestAssistantMessage(o)},decide:o=>{if(n.webMcpApprovalResolvers.has(o))return n.resolveWebMcpApproval(o,"denied");let l=n.messages.find(u=>u.id===o)?.approval;l?.status==="pending"&&n.resolveApproval(l,"denied")}}));let r=i.generation(),c=()=>i.provider()===e&&r===i.generation(),a=(n.config.voiceRecognition??{}).processingErrorText??"Voice processing failed. Please try again.";e.onResult(o=>{c()&&o.provider!=="runtype"&&o.text&&o.text.trim()&&n.sendMessage(o.text,{viaVoice:!0})}),e.onTranscript&&e.onTranscript((o,l,u,p)=>{if(c()){if(p?.turnId){i.keyed?.apply(o,l,u,p.turnId,p.startMs,p.caption);return}if(o==="user"){if(i.pendingUser)n.upsertMessage({id:i.pendingUser,role:"user",content:l,createdAt:new Date().toISOString(),streaming:!1,voiceProcessing:!u});else{let m=n.injectMessage({role:"user",content:l,streaming:!1,voiceProcessing:!u});i.pendingUser=m.id}if(u){i.turns().start(p?.turnId),i.pendingUser=null;let m=n.injectMessage({role:"assistant",content:"",streaming:!0,voiceProcessing:!0});i.pendingAssistant=m.id,i.setStreaming(!0)}}else{if(!i.turns().accepts(p?.turnId))return;if(i.pendingAssistant)n.upsertMessage({id:i.pendingAssistant,role:"assistant",content:l,createdAt:new Date().toISOString(),streaming:!u,voiceProcessing:!u});else{let m=n.injectMessage({role:"assistant",content:l,streaming:!u,voiceProcessing:!u});i.pendingAssistant=m.id}u&&(i.pendingAssistant&&n.ttsSpokenMessageIds.add(i.pendingAssistant),i.setStreaming(!1),i.pendingAssistant=null)}}}),e.onLevel&&e.onLevel(o=>{c()&&i.setLevel(Number.isFinite(o)?Math.max(0,Math.min(1,o)):0)}),e.onMetrics&&e.onMetrics(o=>{c()&&n.config.voiceRecognition?.onMetrics?.(o)}),e.onError(o=>{c()&&(console.error("Voice error:",o),i.pendingAssistant&&(n.upsertMessage({id:i.pendingAssistant,role:"assistant",content:a,createdAt:new Date().toISOString(),streaming:!1,voiceProcessing:!1}),i.setStreaming(!1),i.pendingUser=null,i.pendingAssistant=null),i.keyed?.fail(a))}),e.onStatusChange(o=>{c()&&(i.setStatus(o),(o==="listening"||o==="idle"||o==="disconnected")&&i.settlePending(o!=="listening"),(o==="idle"||o==="disconnected")&&i.keyed?.settle(),n.callbacks.onVoiceStatusChanged?.(o))}),e.connect()}catch(r){console.error("Failed to setup voice:",r)}};var Xe=i=>{let e=a=>{if(i.original)return;let o=i.config().voiceRecognition??{};i.original={backgroundColor:a.style.backgroundColor,color:a.style.color,borderColor:a.style.borderColor,iconName:o.iconName??"mic",iconSize:parseFloat(o.iconSize??"")||i.iconFallbackPx}},t=(a,o,l)=>{let u=a.querySelector("svg");u&&u.remove();let p=i.original?.iconSize??(parseFloat(i.config().voiceRecognition?.iconSize??"")||i.iconFallbackPx),m=i.renderIcon(o,p,l,1.5);m&&a.appendChild(m)},s=a=>{a.classList.remove("persona-voice-recording","persona-voice-processing","persona-voice-speaking")};return{recording:()=>{let a=i.mic();if(!a)return;e(a);let o=i.config().voiceRecognition??{},l=o.recordingBackgroundColor,u=o.recordingIconColor,p=o.recordingBorderColor;if(s(a),a.classList.add("persona-voice-recording"),i.setMicState("recording"),a.style.backgroundColor=l??"var(--persona-voice-recording-bg, #ef4444)",a.style.color=u??"var(--persona-voice-recording-indicator, #ffffff)",u){let m=a.querySelector("svg");m&&m.setAttribute("stroke",u)}p&&(a.style.borderColor=p),a.setAttribute("aria-label","Stop voice recognition")},processing:()=>{let a=i.mic();if(!a)return;e(a);let o=i.config().voiceRecognition??{},l=i.session().getVoiceInterruptionMode(),u=i.original,p=o.processingIconName??"loader",m=o.processingIconColor??u?.color??"",h=o.processingBackgroundColor??u?.backgroundColor??"",f=o.processingBorderColor??u?.borderColor??"";s(a),a.classList.add("persona-voice-processing"),i.setMicState("processing"),a.style.backgroundColor=h,a.style.borderColor=f;let w=m||"currentColor";a.style.color=w,t(a,p,w),a.setAttribute("aria-label","Processing voice input"),l==="none"&&(a.style.cursor="default")},speaking:()=>{let a=i.mic();if(!a)return;e(a);let o=i.config().voiceRecognition??{},l=i.session().getVoiceInterruptionMode(),u=i.original,p=l==="cancel"?"square":l==="barge-in"?"mic":"volume-2",m=o.speakingIconName??p,h=o.speakingIconColor??(l==="barge-in"?o.recordingIconColor??u?.color??"":u?.color??""),f=o.speakingBackgroundColor??(l==="barge-in"?o.recordingBackgroundColor??"var(--persona-voice-recording-bg, #ef4444)":u?.backgroundColor??""),w=o.speakingBorderColor??(l==="barge-in"?o.recordingBorderColor??"":u?.borderColor??"");s(a),a.classList.add("persona-voice-speaking"),i.setMicState("speaking"),a.style.backgroundColor=f,a.style.borderColor=w;let y=h||"currentColor";a.style.color=y,t(a,m,y);let k=l==="cancel"?"Stop playback and re-record":l==="barge-in"?"Speak to interrupt":"Agent is speaking";a.setAttribute("aria-label",k),l==="none"&&(a.style.cursor="default"),l==="barge-in"&&a.classList.add("persona-voice-recording")},reset:()=>{let a=i.mic();if(!a)return;s(a),i.setMicState("idle");let o=i.original;o&&(a.style.backgroundColor=o.backgroundColor??"",a.style.color=o.color??"",a.style.borderColor=o.borderColor??"",t(a,o.iconName,o.color||"currentColor"),i.original=null),a.style.cursor="",a.setAttribute("aria-label","Start voice recognition")}}};export{T as BrowserVoiceProvider,$ as KeyedVoiceTranscript,_ as RuntypeVoiceProvider,se as createBestAvailableVoiceProvider,Xe as createMicStateStyles,j as createVoiceProvider,He as createVoiceSessionBridge,Be as isVoiceSupported,ze as wireSessionVoice};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runtypelabs/persona",
3
- "version": "4.26.1",
3
+ "version": "4.27.0",
4
4
  "description": "Themeable, pluggable streaming agent widget for websites, in plain JS with support for voice input and reasoning / tool output.",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -1248,13 +1248,20 @@ export async function streamResponse(
1248
1248
  } else {
1249
1249
  // Buffered / dispatch-mode step: no streamed text block, but the step
1250
1250
  // carries the final response (and/or a stopReason) — render it as the
1251
- // assistant message. An empty response + stopReason still surfaces a
1252
- // sealed bubble so the UI can show an affordance.
1251
+ // assistant message. An empty response + a stopReason that warrants a
1252
+ // notice still surfaces a sealed bubble so the UI can show it. A
1253
+ // natural stop (`end_turn` / `unknown`) has no notice, so an empty
1254
+ // step adds nothing: a resumed leg that ends the turn after
1255
+ // `suggest_replies` must not leave an empty bubble behind the answer.
1253
1256
  const hasResponse =
1254
1257
  finalResponse !== undefined &&
1255
1258
  finalResponse !== null &&
1256
1259
  finalResponse !== "";
1257
- if (hasResponse || flowStopReason) {
1260
+ const hasNotice =
1261
+ !!flowStopReason &&
1262
+ flowStopReason !== "end_turn" &&
1263
+ flowStopReason !== "unknown";
1264
+ if (hasResponse || hasNotice) {
1258
1265
  const assistant = ensureAssistantMessage();
1259
1266
  if (flowStopReason) assistant.stopReason = flowStopReason;
1260
1267
  if (hasResponse) {
@@ -2271,6 +2271,26 @@ describe('AgentWidgetClient - stopReason propagation', () => {
2271
2271
  expect(final!.stopReason).toBe('max_tool_calls');
2272
2272
  });
2273
2273
 
2274
+ it.each(['end_turn', 'unknown'] as const)(
2275
+ 'adds no assistant message for an empty response with a natural stopReason=%s',
2276
+ async (stopReason) => {
2277
+ // A resumed leg that ends the turn after suggest_replies streams no text:
2278
+ // a natural stop has no notice, so an empty bubble would only sit behind the answer.
2279
+ const events = await runDispatch([
2280
+ `data: ${JSON.stringify({
2281
+ type: 'step_complete',
2282
+ id: 'agent-response',
2283
+ stepType: 'prompt',
2284
+ success: true,
2285
+ result: { response: '' },
2286
+ stopReason,
2287
+ })}\n\n`,
2288
+ `data: ${JSON.stringify({ type: 'execution_complete', success: true })}\n\n`,
2289
+ ]);
2290
+ expect(events.some((e) => e.type === 'message' && e.message.role === 'assistant')).toBe(false);
2291
+ }
2292
+ );
2293
+
2274
2294
  it('turn_complete.stopReason overrides any earlier step_complete value (agent-loop path)', async () => {
2275
2295
  // Build an agent-mode stream that emits both events. turn_complete
2276
2296
  // arrives last; its stopReason should win.
package/src/session.ts CHANGED
@@ -556,6 +556,11 @@ export class AgentWidgetSession {
556
556
  return this.#voiceStatus;
557
557
  }
558
558
 
559
+ /** The voice call's pending status-line notice (why it ended, or reconnecting), once. */
560
+ public getVoiceNotice(): string | null {
561
+ return this.#voiceProvider?.takeNotice?.() ?? null;
562
+ }
563
+
559
564
  /** The live call's AI-disclosure notice, if it shows one. */
560
565
  public getVoiceDisclosure(): string | null {
561
566
  return this.#voiceProvider?.getDisclosure?.() ?? null;
@@ -36,7 +36,16 @@ const h = vi.hoisted(() => {
36
36
  errorCb: ((e: Error) => void) | null;
37
37
  bridge: VoiceSessionBridge | null;
38
38
  prewarms: number;
39
- } = { transcriptCb: null, metricsCb: null, statusCb: null, errorCb: null, bridge: null, prewarms: 0 };
39
+ notice: string | null;
40
+ } = {
41
+ transcriptCb: null,
42
+ metricsCb: null,
43
+ statusCb: null,
44
+ errorCb: null,
45
+ bridge: null,
46
+ prewarms: 0,
47
+ notice: null,
48
+ };
40
49
 
41
50
  const fakeProvider = {
42
51
  type: 'runtype' as const,
@@ -64,6 +73,11 @@ const h = vi.hoisted(() => {
64
73
  setSessionBridge: (bridge: VoiceSessionBridge) => {
65
74
  state.bridge = bridge;
66
75
  },
76
+ takeNotice: () => {
77
+ const notice = state.notice;
78
+ state.notice = null;
79
+ return notice;
80
+ },
67
81
  };
68
82
 
69
83
  return { state, fakeProvider };
@@ -513,12 +527,31 @@ describe('AgentWidgetSession - voice client delegation bridge', () => {
513
527
  });
514
528
 
515
529
  it('hands the provider a bridge whose history is the visible settled messages', () => {
516
- drive('user', 'still talk', false, 'u1'); // interim: not part of the context
517
- // An earlier call's captions (filler, small talk) aren't conversation either.
530
+ // An earlier call's captions (e.g. on the previous page) are conversation
531
+ // the new voice session never heard, so they carry over.
518
532
  h.state.transcriptCb!('assistant', 'Let me check.', true, { turnId: 'f0', caption: true });
519
533
  h.state.transcriptCb!('user', 'hi there', true, { turnId: 'u0', caption: true });
534
+ drive('user', 'still talk', false, 'u1'); // interim: not part of the context
535
+ expect(h.state.bridge!.getHistory()).toEqual([
536
+ { role: 'assistant', content: 'Welcome! How can I help?' },
537
+ { role: 'assistant', content: 'Let me check.' },
538
+ { role: 'user', content: 'hi there' },
539
+ ]);
540
+ });
541
+
542
+ it("leaves a delegated turn's spoken filler out of the history, keeping the answer", async () => {
543
+ // Full duplex: the voice model's words arrive as captions.
544
+ h.state.transcriptCb!('user', 'whats the weather in paris', true, { turnId: 'u1', caption: true });
545
+ h.state.transcriptCb!('assistant', 'Let me check that.', true, { turnId: 'f1', caption: true });
546
+ dispatch.mockImplementation(async (_options, onEvent) => reply(onEvent, 'It is sunny in Paris.'));
547
+ await h.state.bridge!.runDelegatedTurn(req({ delegationId: 'd1', userText: 'whats the weather in paris' }));
548
+ h.state.transcriptCb!('assistant', 'Anything else?', true, { turnId: 'a2', caption: true });
549
+
520
550
  expect(h.state.bridge!.getHistory()).toEqual([
521
551
  { role: 'assistant', content: 'Welcome! How can I help?' },
552
+ { role: 'user', content: 'whats the weather in paris' },
553
+ { role: 'assistant', content: 'It is sunny in Paris.' },
554
+ { role: 'assistant', content: 'Anything else?' },
522
555
  ]);
523
556
  });
524
557
 
@@ -1449,3 +1482,26 @@ describe('AgentWidgetSession - prewarmVoice', () => {
1449
1482
  expect(h.state.prewarms).toBe(0);
1450
1483
  });
1451
1484
  });
1485
+
1486
+ describe('AgentWidgetSession - voice session end (Amendment 6)', () => {
1487
+ const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
1488
+ const tokenSession = () =>
1489
+ new AgentWidgetSession(
1490
+ {
1491
+ clientToken: 'ct_live_demo',
1492
+ apiUrl: 'https://api.runtype.com',
1493
+ voiceRecognition: { enabled: true, provider: { type: 'runtype', runtype: { agentId: 'a1' } } },
1494
+ },
1495
+ { onMessagesChanged: () => {}, onStatusChanged: () => {}, onStreamingChanged: () => {}, onError: () => {} },
1496
+ );
1497
+
1498
+ it("hands the UI the provider's status-line notice once", async () => {
1499
+ const session = tokenSession();
1500
+ session.setupVoice();
1501
+ await flush();
1502
+ h.state.notice = 'This voice call reached its time limit.';
1503
+ expect(session.getVoiceNotice()).toBe('This voice call reached its time limit.');
1504
+ expect(session.getVoiceNotice()).toBeNull();
1505
+ });
1506
+
1507
+ });
package/src/types.ts CHANGED
@@ -5230,6 +5230,11 @@ export interface VoiceProvider {
5230
5230
  setSessionBridge?(bridge: VoiceSessionBridge): void;
5231
5231
  /** The AI-disclosure notice for the live call, if one should show (speech-to-speech). */
5232
5232
  getDisclosure?(): string | null;
5233
+ /**
5234
+ * Status-line text for the call, returned once: why the server ended it
5235
+ * (`session_end`) or that it is reconnecting. `null` when there is none.
5236
+ */
5237
+ takeNotice?(): string | null;
5233
5238
  }
5234
5239
 
5235
5240
  /** Metadata on a {@link VoiceProvider.onTranscript} update. */
@@ -159,6 +159,23 @@ describe("streamingSubmitBehavior: defer-one", () => {
159
159
  expect(status?.textContent).toContain("already queued");
160
160
  });
161
161
 
162
+ it("keeps a notice in the status line when the chat status changes under it", async () => {
163
+ const { mount } = makeController();
164
+ await startStreaming(mount);
165
+ type(mount, "queued message");
166
+ pressEnter(mount);
167
+ await flush();
168
+ type(mount, "second attempt");
169
+ pressEnter(mount);
170
+ await flush();
171
+
172
+ stream.push(unifiedFrames());
173
+ stream.close();
174
+ await flush(50);
175
+ const status = mount.querySelector<HTMLElement>("[data-persona-composer-notice]");
176
+ expect(status?.textContent).toContain("already queued");
177
+ });
178
+
162
179
  it("card Edit restores the snapshot into the draft and drops the card", async () => {
163
180
  const { mount, controller } = makeController();
164
181
  await startStreaming(mount);
package/src/ui.ts CHANGED
@@ -353,11 +353,14 @@ import {
353
353
  // Default localStorage key for chat history (automatically cleared on clear chat)
354
354
  const DEFAULT_CHAT_HISTORY_STORAGE_KEY = "persona-chat-history";
355
355
  const VOICE_STATE_RESTORE_WINDOW = 30 * 1000;
356
+ const VOICE_RESUME_NOTICE = "Tap the mic to resume your voice call";
356
357
  // Split desktop boundary; must match widget.css artifact media queries (min-width:641px).
357
358
  const ARTIFACT_SPLIT_DESKTOP_MIN = 641;
358
359
 
359
360
  /** Marks the status region while a composer lock reason owns it. */
360
361
  const COMPOSER_REASON_ATTR = "data-persona-composer-reason";
362
+ /** Marks the status region while a transient composer notice owns it. */
363
+ const COMPOSER_NOTICE_ATTR = "data-persona-composer-notice";
361
364
 
362
365
  const IMAGE_FILE_EXTENSION_BY_MIME_TYPE: Record<string, string> = {
363
366
  "image/png": "png",
@@ -1384,8 +1387,9 @@ export const createAgentExperience = (
1384
1387
  placeStatusIndicator(el, composerForm, statusCfg);
1385
1388
  applyStatusIndicatorState(el, statusCfg, status);
1386
1389
  }
1387
- // A composer lock reason owns the status region until the lock clears.
1388
- if (el.hasAttribute(COMPOSER_REASON_ATTR)) return;
1390
+ // A composer lock reason owns the status region until the lock clears, and
1391
+ // a notice until its timer ends (which re-applies the status).
1392
+ if (el.hasAttribute(COMPOSER_REASON_ATTR) || el.hasAttribute(COMPOSER_NOTICE_ATTR)) return;
1389
1393
  if (status === "idle" && statusCfg.idleLink) {
1390
1394
  el.textContent = "";
1391
1395
  const link = document.createElement("a");
@@ -5083,6 +5087,15 @@ export const createAgentExperience = (
5083
5087
  // would otherwise not appear until the first keystroke.
5084
5088
  syncComposerCompact();
5085
5089
  const voiceAutoResumeMode = config.voiceRecognition?.autoResume ?? false;
5090
+ // A session-voice call is live while listening OR while its continuous
5091
+ // (barge-in) mic is hot: a full-duplex call cycles listening → processing →
5092
+ // speaking, and `isVoiceActive()` alone (status === listening) would record a
5093
+ // live call as hung up and never restore it on the next page.
5094
+ const isSessionVoiceLive = () => session.isVoiceActive() || session.isBargeInActive();
5095
+ // Set on `pagehide`: the unload tears the call down without the visitor
5096
+ // hanging up, so its trailing status changes must not overwrite the
5097
+ // persisted "call was live" record the next page restores from.
5098
+ let pageHiding = false;
5086
5099
  const emitVoiceState = (source: AgentWidgetVoiceStateEvent["source"]) => {
5087
5100
  eventBus.emit("voice:state", {
5088
5101
  active: voiceState.active,
@@ -5112,17 +5125,37 @@ export const createAgentExperience = (
5112
5125
  voiceState.manuallyDeactivated = false;
5113
5126
  if (usesSessionVoice(config.voiceRecognition?.provider)) {
5114
5127
  session.toggleVoice().then(() => {
5115
- voiceState.active = session.isVoiceActive();
5128
+ voiceState.active = isSessionVoiceLive();
5116
5129
  emitVoiceState("restore");
5117
- if (session.isVoiceActive()) applyRuntypeMicRecordingStyles();
5130
+ if (voiceState.active) {
5131
+ applyRuntypeMicRecordingStyles();
5132
+ } else {
5133
+ // Typically the autoplay policy: this page has no user gesture
5134
+ // yet, so the audio graph can't start. A mic tap resumes it.
5135
+ persistVoiceMetadata();
5136
+ showComposerNotice(VOICE_RESUME_NOTICE, 10_000);
5137
+ }
5118
5138
  });
5119
5139
  } else {
5120
5140
  startVoiceRecognition("restore");
5121
5141
  }
5122
5142
  }
5123
- }, 1000);
5143
+ // Session voice redials at once (a navigation mid-call should feel
5144
+ // seamless); toggleVoice() already waits on the provider setup.
5145
+ }, usesSessionVoice(config.voiceRecognition?.provider) ? 0 : 1000);
5124
5146
  }
5125
5147
  };
5148
+ // Re-stamp a live call as the page unloads: the restore window counts from
5149
+ // the navigation, not from when the (possibly long) call was started. A
5150
+ // back/forward-cache hide (`persisted`) isn't an unload: the page lives on.
5151
+ const handleVoicePageHide = (event: PageTransitionEvent) => {
5152
+ pageHiding = !event.persisted;
5153
+ if (voiceState.active) persistVoiceMetadata();
5154
+ };
5155
+ if (typeof window !== "undefined") {
5156
+ window.addEventListener("pagehide", handleVoicePageHide);
5157
+ destroyCallbacks.push(() => window.removeEventListener("pagehide", handleVoicePageHide));
5158
+ }
5126
5159
 
5127
5160
  const getMessagesForPersistence = () =>
5128
5161
  session
@@ -7916,6 +7949,7 @@ export const createAgentExperience = (
7916
7949
  statusText.setAttribute("role", "status");
7917
7950
  statusText.setAttribute("aria-live", "polite");
7918
7951
  statusText.setAttribute(COMPOSER_REASON_ATTR, "");
7952
+ statusText.removeAttribute(COMPOSER_NOTICE_ATTR); // the reason outranks a notice
7919
7953
  statusText.textContent = reason;
7920
7954
  applyStatusIndicatorState(statusText, config.statusIndicator ?? {}, "idle");
7921
7955
  return;
@@ -8803,6 +8837,9 @@ export const createAgentExperience = (
8803
8837
  // provider; the mic-button styling below is runtype-specific.
8804
8838
  eventBus.emit("voice:status", { status, timestamp: Date.now() });
8805
8839
  if (status !== 'disconnected' && !usesSessionVoice(config.voiceRecognition?.provider)) return;
8840
+ // Why the server ended the call, or that it is reconnecting.
8841
+ const voiceNotice = session.getVoiceNotice();
8842
+ if (voiceNotice) showComposerNotice(voiceNotice, 10_000);
8806
8843
 
8807
8844
  switch (status) {
8808
8845
  case 'listening':
@@ -8852,7 +8889,7 @@ export const createAgentExperience = (
8852
8889
  }
8853
8890
  removeRuntypeMicStateStyles();
8854
8891
  emitVoiceState("system");
8855
- persistVoiceMetadata();
8892
+ if (!pageHiding) persistVoiceMetadata();
8856
8893
  }
8857
8894
  break;
8858
8895
  }
@@ -9507,7 +9544,6 @@ export const createAgentExperience = (
9507
9544
  resetHistoryNavigation();
9508
9545
  };
9509
9546
 
9510
- const COMPOSER_NOTICE_ATTR = "data-persona-composer-notice";
9511
9547
  let composerNoticeTimer: ReturnType<typeof setTimeout> | null = null;
9512
9548
 
9513
9549
  /** Transient message in the status region; a lock reason always outranks it. */
@@ -10497,11 +10533,11 @@ export const createAgentExperience = (
10497
10533
  }
10498
10534
 
10499
10535
  session.toggleVoice().then(() => {
10500
- voiceState.active = session.isVoiceActive();
10501
- voiceState.manuallyDeactivated = !session.isVoiceActive();
10536
+ voiceState.active = isSessionVoiceLive();
10537
+ voiceState.manuallyDeactivated = !voiceState.active;
10502
10538
  persistVoiceMetadata();
10503
10539
  emitVoiceState("user");
10504
- if (session.isVoiceActive()) {
10540
+ if (voiceState.active) {
10505
10541
  applyRuntypeMicRecordingStyles();
10506
10542
  } else {
10507
10543
  removeRuntypeMicStateStyles();
@@ -12956,9 +12992,10 @@ export const createAgentExperience = (
12956
12992
  voiceState.manuallyDeactivated = false;
12957
12993
  persistVoiceMetadata();
12958
12994
  session.toggleVoice().then(() => {
12959
- voiceState.active = session.isVoiceActive();
12995
+ voiceState.active = isSessionVoiceLive();
12996
+ persistVoiceMetadata();
12960
12997
  emitVoiceState("user");
12961
- if (session.isVoiceActive()) applyRuntypeMicRecordingStyles();
12998
+ if (voiceState.active) applyRuntypeMicRecordingStyles();
12962
12999
  });
12963
13000
  return true;
12964
13001
  }
@@ -0,0 +1,6 @@
1
+ {
2
+ "type": "session_end",
3
+ "reason": "provider_error",
4
+ "message": "The voice provider disconnected.",
5
+ "retryable": true
6
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "type": "session_end",
3
+ "reason": "maintenance_window"
4
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "type": "session_end",
3
+ "reason": "max_duration",
4
+ "retryable": true
5
+ }