@runtypelabs/persona 4.27.0 → 4.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/approval-ui.d.cts +13 -0
- package/dist/approval-ui.d.ts +13 -0
- package/dist/artifacts-ui.d.cts +13 -0
- package/dist/artifacts-ui.d.ts +13 -0
- package/dist/chunk-AALCT54S.js +1 -0
- package/dist/{client-history-HTRKHBAF.js → client-history-LV3KHEFA.js} +1 -1
- package/dist/client-history.js +1 -1
- package/dist/codegen.cjs +1 -1
- package/dist/codegen.js +1 -1
- package/dist/context-mentions.d.cts +13 -0
- package/dist/context-mentions.d.ts +13 -0
- package/dist/event-stream-view.d.cts +13 -0
- package/dist/event-stream-view.d.ts +13 -0
- package/dist/forms-ui.d.cts +19 -0
- package/dist/forms-ui.d.ts +19 -0
- package/dist/index.cjs +33 -33
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.global.js +1 -1
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +27 -27
- package/dist/index.js.map +1 -1
- package/dist/session-reconnect.d.cts +13 -0
- package/dist/session-reconnect.d.ts +13 -0
- package/dist/smart-dom-reader.d.cts +13 -0
- package/dist/smart-dom-reader.d.ts +13 -0
- package/dist/theme-editor-preview.cjs +63 -63
- package/dist/theme-editor-preview.d.cts +13 -0
- package/dist/theme-editor-preview.d.ts +13 -0
- package/dist/theme-editor-preview.js +5 -5
- package/dist/theme-editor.d.cts +13 -0
- package/dist/theme-editor.d.ts +13 -0
- package/dist/voice-runtime-LVUMPUDS.js +17 -0
- package/dist/voice-runtime.cjs +8 -8
- package/dist/voice-runtime.d.cts +24 -0
- package/dist/voice-runtime.d.ts +24 -0
- package/dist/voice-runtime.js +8 -8
- package/package.json +1 -1
- package/src/client.interrupt.test.ts +18 -0
- package/src/client.ts +7 -0
- package/src/session.ts +11 -3
- package/src/session.voice.test.ts +140 -0
- package/src/types.ts +19 -0
- package/src/voice/runtype-voice-provider.ts +93 -11
- package/src/voice/voice-clause-buffer.test.ts +143 -0
- package/src/voice/voice-clause-buffer.ts +187 -0
- package/src/voice/voice-delegation.ts +85 -3
- package/src/voice/voice.test.ts +257 -2
- package/dist/chunk-3255TNY7.js +0 -1
- package/dist/voice-runtime-RHRX4F3T.js +0 -17
package/dist/voice-runtime.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
var
|
|
1
|
+
var L=class{#e=null;#i=0;#r=[];#n=[];#c=[];#l=!1;#m=!1;#y=0;#o=!1;#p=!1;#d;#t=[];#b=0;#_=!1;#a;#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.#d=this.#g>0}#w(){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 i=new Uint8Array(this.#h.length+e.length);i.set(this.#h),i.set(e,this.#h.length),t=i,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.#f(t);s.length!==0&&(this.#d?(this.#t.push(s),this.#b+=s.length,clearTimeout(this.#a),this.#b>=this.#g?this.#S():this.#_&&(this.#a=setTimeout(()=>this.#S(),this.#g/this.#u*1e3))):this.#A(s))}markStreamEnd(){this.#t.length>0&&this.#S(),this.#m=!0,this.#k()}flush(){for(let e of this.#r){e.onended=null;try{e.stop(),e.disconnect()}catch{}}this.#r=[],this.#y=0,this.#i=0,this.#l=!1,this.#m=!1,this.#n=[],this.#c=[],this.#h=null,this.#t=[],this.#b=0,this.#d=this.#g>0,this.#o=!1,clearTimeout(this.#a)}setContinuousMode(e){this.#_=e}isPlaying(){return this.#l}onFinished(e){this.#n.push(e)}onStarted(e){this.#c.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)}#S(){this.#d=!1;let e=this.#t;this.#t=[],this.#b=0;for(let t of e)this.#A(t)}#A(e){if(e.length===0)return;let t=this.#w(),s=t.createBuffer(1,e.length,this.#u);s.getChannelData(0).set(e);let i=t.createBufferSource();i.buffer=s,i.connect(t.destination);let r=t.currentTime;if(this.#i===0?this.#i=r:this.#i<r&&(this.#i=r,this.#g>0&&(this.#d=!0)),i.start(this.#i),this.#i+=s.duration,this.#r.push(i),this.#y++,this.#l=!0,!this.#o){this.#o=!0;let l=this.#c.slice();this.#c=[];for(let d of l)d()}i.onended=()=>{let l=this.#r.indexOf(i);l!==-1&&(this.#r.splice(l,1),this.#y--,this.#k())}}#k(){if(this.#m&&this.#y<=0&&this.#t.length===0){this.#l=!1,this.#m=!1;let e=this.#n.slice();this.#n=[];for(let t of e)t()}}#f(e){let t=Math.floor(e.length/2),s=new Float32Array(t),i=new DataView(e.buffer,e.byteOffset,e.byteLength);for(let r=0;r<t;r++){let l=i.getInt16(r*2,!0);s[r]=l/32768}return s}};var ae="4.28.0";var ce=ae;var Y=16e3,_e=24e3,Ee=4096,xe=Y*2*8,Ie=4,Pe=1380533830,Ve=3e4,Re=1e3,je=5e3,Le=3e4,Ne=3e4,Oe=6e5,Ue="full-duplex-v1",De=300,Be=2e3,qe=2e3,$e="runtype-browser-v1",We=54e4,He="You're talking to an AI assistant. Voice is processed by OpenAI.",Fe=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."]]),le=["provider_error","server_restart"],N="Voice connection lost.",ze=8e3,Xe=4e3,Ge=12,de=2e3;function Ke(n,e){let t="Conversation so far:",s=e.slice(0,Xe).trim(),i=ze-(s?s.length+2:0)-t.length,r=[];for(let d of n.slice(-Ge).reverse()){let a=d.content.replace(/\s+/g," ").trim();if(!a)continue;a.length>de&&(a=`${a.slice(0,de-1)}\u2026`);let o=`${d.role==="user"?"User":"Assistant"}: ${a}`;if(o.length+1>i)break;i-=o.length+1,r.unshift(o)}let l=r.length>0?[`${t}
|
|
2
2
|
${r.join(`
|
|
3
|
-
`)}`]:[];return s&&
|
|
3
|
+
`)}`]:[];return s&&l.push(s),l.join(`
|
|
4
4
|
|
|
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}
|
|
5
|
+
`)}function Ye(n){return n.byteLength>=44&&new DataView(n).getUint32(0,!1)===Pe?new Uint8Array(n,44):new Uint8Array(n)}function ue(n){let e=n.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 Je(n){return ue(n).replace(/^ws/i,"http")}function Ze(n){return typeof n!="number"||!Number.isFinite(n)?Le:Math.min(Oe,Math.max(Ne,Math.round(n)))}var V=class{constructor(e){this.config=e;this.type="runtype";this.#e=null;this.#i=null;this.#r=[];this.#n=null;this.#c=null;this.#l=null;this.#m=[];this.#y=0;this.#o=null;this.#p=!1;this.#d=!1;this.#t=0;this.#b=!1;this.#_=Number.NEGATIVE_INFINITY;this.#a=null;this.#u=!1;this.#g=!1;this.#h=0;this.#w=0;this.#A=!1;this.bridge=null;this.#k=!1;this.#f=new Set;this.#E=new Map;this.#x=new Set;this.#j=null;this.#C=null;this.#U=!1;this.#D=!1;this.#B=!1;this.#L=null;this.#q=null;this.#$=Promise.resolve();this.#T=new Set;this.#I=new Set;this.#W=null;this.#v=!1;this.#P=new Set;this.#V=new Set;this.#Q=[];this.#H=[];this.#F=[];this.#z=[];this.#X=[]}#e;#i;#r;#n;#c;#l;#m;#y;#o;#p;#d;#t;#b;#_;#a;#u;#g;#h;#w;#S;#A;#k;#f;#E;#x;#O;#j;#C;#Z;#U;#D;#B;#L;#q;#$;#T;#I;#W;#v;#P;#V;#Q;#H;#F;#z;#X;async connect(){}setSessionBridge(e){this.bridge=e}prewarm(){if(this.#p||this.#a)return;let e=this.config?.agentId,t=this.config?.clientToken,s=this.config?.host;if(!e||!t||!s)return;let i=Date.now();if(!(i-this.#_<Ve)){this.#_=i;try{this.config?.prewarmMode==="attach"?this.#de(s,e,t):this.#le(s,e,t)}catch{}}}#le(e,t,s){if(typeof fetch!="function")return;let i=`${Je(e)}/v1/client/agents/${encodeURIComponent(t)}/voice/prewarm`;fetch(i,{method:"POST",headers:{Authorization:`Bearer ${s}`},keepalive:!0}).catch(()=>{})}#de(e,t,s){let i=new URLSearchParams,r=this.config?.attachIdleMs,l=Ze(r);r!==void 0&&i.set("attachIdleMs",String(l));let d=new WebSocket(this.#te(e,t,i),["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.#a=c,d.onopen=()=>{this.#a===c&&(c.state=d.protocol==="runtype.attach"?"attached":"live",c.openedAt=Date.now(),this.#ee(c,l),c.state==="attached"&&d.send('{"type":"ping"}'),c.settle())},d.onmessage=u=>{if(this.#a!==c||typeof u.data!="string")return;let p;try{p=JSON.parse(u.data)}catch{return}p.type==="attached"&&typeof p.idleMs=="number"&&this.#ee(c,p.idleMs-(Date.now()-c.openedAt))},d.onerror=()=>{},d.onclose=()=>{this.#a===c&&this.#N()}}#ee(e,t){clearTimeout(e.idleTimer),e.idleTimer=setTimeout(()=>{this.#a===e&&this.#N()},Math.max(0,t))}#N(){let e=this.#a;if(!e)return;this.#a=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#ue(){let e=this.#a;if(!e)return null;if(e.state==="connecting"){let t;await Promise.race([e.ready,new Promise(s=>{t=setTimeout(s,je)})]),clearTimeout(t)}return this.#a!==e?null:e.state==="connecting"||e.ws.readyState!==WebSocket.OPEN?(this.#N(),null):(this.#a=null,clearTimeout(e.idleTimer),{ws:e.ws,live:e.state==="live"})}#te(e,t,s){let i=["partial_transcript","context"];this.bridge&&this.config?.clientDelegation!==!1&&i.push("client_delegation","delegation_update","delegation_read_back","delegation_stream"),s&&i.unshift("attach");let r=new URLSearchParams({voiceProtocol:$e,clientVersion:`persona/${ce}`,voiceCapabilities:Ue,clientCapabilities:i.join(",")});return s?.forEach((l,d)=>r.set(d,l)),`${ue(e)}/ws/agents/${encodeURIComponent(t)}/voice?${r}`}async startListening(){if(this.#p)return;let e=this.#D;e||(this.#U=!1,this.#C=null),this.#D=!1;let t=this.config?.agentId,s=this.config?.clientToken,i=this.config?.host;if(!t)throw new Error("Runtype voice requires an agentId");if(!s)throw new Error("Runtype voice requires a clientToken");if(!i)throw new Error("Runtype voice requires a host (or widget apiUrl)");let r=++this.#t;this.#b=!1,this.#p=!0;try{let l=await navigator.mediaDevices.getUserMedia({audio:{sampleRate:Y,channelCount:1,echoCancellation:!0}});if(r!==this.#t){l.getTracks().forEach(p=>p.stop());return}this.#n=l;let d=window.AudioContext||window.webkitAudioContext,a=new d({sampleRate:Y});if(this.#i=a,a.state==="suspended"){if(await Promise.race([a.resume().catch(()=>{}),new Promise(p=>setTimeout(p,e?500:Re))]),r!==this.#t)return;if(a.state!=="running")throw e?new Error(this.#C=`${N} Tap the mic to reconnect.`):new Error("Audio blocked until a tap")}let o=this.config?.createPlaybackEngine?await this.config.createPlaybackEngine():new L(_e);if(r!==this.#t){o.destroy(),l.getTracks().forEach(p=>p.stop()),a.close().catch(()=>{});return}this.#o=o,this.#Y(),this.#he(a,l,r);let c=await this.#ue();if(r!==this.#t){c?.ws.close(1e3,"client ended call");return}if(c){let{ws:p}=c;this.#e=p,this.#se(p,r),c.live||p.send('{"type":"start"}'),this.#G(p),this.#s("listening");return}let u=new WebSocket(this.#te(i,t),["runtype.bearer",s]);u.binaryType="arraybuffer",this.#e=u,u.onopen=()=>{r===this.#t&&(this.#G(u),this.#s("listening"))},this.#se(u,r)}catch(l){throw this.#M(),this.#R(l),this.#s("error"),l}}#se(e,t){e.onmessage=s=>this.#ge(s,t),e.onerror=()=>{t!==this.#t||this.#j||(this.#R(new Error("Voice connection failed")),this.#s("error"),this.#M())},e.onclose=s=>{if(this.#b){this.#b=!1;return}if(t!==this.#t)return;let i=this.#j??(s.code===4408?{reason:"idle_timeout"}:null);if(!i){if(s.code!==1e3){let r=s.code?` (code ${s.code})`:"";this.#R(new Error(`Voice connection closed${r}`)),this.#s("error")}else this.#s("idle");this.#M();return}this.#pe(i.reason,i.message)}}#pe(e,t){let s=le.includes(e)&&!this.#U;if(this.#M(),s){this.#U=!0,this.#p=!0,this.#C=e==="provider_error"?`${N} Reconnecting\u2026`:"Reconnecting\u2026",this.#Z=setTimeout(()=>{this.#p=!1,this.#D=!0,this.startListening().catch(()=>{})},1e3+Math.random()*1e3),this.#s("idle");return}if(le.includes(e))return this.#C=N,this.#R(new Error(N)),this.#s("error");if(e==="error")return this.#s("error");this.#C=e==="quota"&&t||Fe.get(e)||null,this.#s("idle")}takeNotice(){let e=this.#C;return this.#C=null,e}getDisclosure(){let e=this.config?.disclosureText;return this.#p&&this.#u&&e!==!1?e||He:null}async stopListening(){this.#M(),this.#s("idle")}async disconnect(){this.#M(),this.#s("disconnected"),this.#Q=[],this.#H=[],this.#F=[],this.#z=[],this.#X=[]}stopPlayback(){this.#ce();let e=this.#e;e&&e.readyState===WebSocket.OPEN&&(this.#u&&(this.#h=Date.now()+Be,e.send('{"type":"cancel"}')),this.#s("listening"))}getInterruptionMode(){return"barge-in"}isBargeInActive(){return this.#p}onLevel(e){this.#r.push(e)}async deactivateBargeIn(){this.#M(),this.#s("idle")}#he(e,t,s){let i=e.createMediaStreamSource(t);this.#c=i;let r=e.createScriptProcessor(Ee,1,1);this.#l=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 p=0;p<o.length;p++){let m=Math.max(-1,Math.min(1,o[p]));u+=m*m,c[p]=m<0?m*32768:m*32767}if(d&&a)this.#G(d),d.send(c.buffer);else for(this.#m.push(c.buffer),this.#y+=c.byteLength;this.#y>xe;)this.#y-=this.#m.shift().byteLength;if(this.#r.length>0){let p=Math.sqrt(u/o.length),m=Math.max(0,Math.min(1,p*Ie));for(let g of this.#r)g(m)}},i.connect(r),r.connect(e.destination)}#G(e){for(let t of this.#m)e.send(t);this.#m=[],this.#y=0}#ge(e,t){if(t!==this.#t)return;if(e.data instanceof ArrayBuffer){this.#ye(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.#o?.setContinuousMode?.(this.#u)),Array.isArray(s.capabilities)&&(this.#f=new Set(s.capabilities)),typeof s.callId=="string"&&(this.#O=s.callId),this.#k=this.#u&&this.#f.has("client_delegation")&&!!this.bridge,this.#u&&!this.#d&&this.#s("listening"),this.#f.has("context")&&!this.#B&&(this.#B=!0,this.#L=this.#fe());break;case"transcript_update":{let i=s.role==="assistant"?"assistant":"user",r=s.utteranceId??s.turnId;if(!r||i==="assistant"&&this.#K())break;let l=String(r);if(i==="user")s.final===!0&&this.#ie(t),this.#v=!1;else{if(this.#f.has("delegation_read_back")){let d=s.delegationId;typeof d=="string"&&this.#T.has(d)&&!this.#P.has(l)?(this.#V.add(l),this.#v=!1):this.#v&&!this.#P.has(l)&&(this.#V.add(l),this.#v=!1)}else this.#v&&!this.#P.has(l)&&(this.#V.add(l),this.#v=!1);if(this.#P.add(l),this.#V.has(l))break}this.#J(i,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 i=String(s.delegationId??s.turnId);if(s.type==="delegation_started"){this.#ie(t);let r=s.input;this.#k&&r&&this.#me(i,{delegationId:i,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.#T.has(i)&&!this.#I.delete(i)&&!this.#f.has("delegation_read_back")&&this.#re(i);this.#g=s.type==="delegation_started",!this.#d&&!this.#K()&&this.#s(this.#g?"processing":"listening");break}case"delegation_progress":{let i=String(s.delegationId);this.#T.has(i)&&!this.#I.has(i)&&!this.#x.has(i)&&(this.#I.add(i),this.#re(i));break}case"delegation_cancelled":this.#oe(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.#oe(String(s.delegationId));break;case"audio_clear":this.#ce(),this.#h=0,this.#s("listening");break;case"transcript_interim":this.#s("listening"),this.#J("user",s.text??"",!1);break;case"transcript_final":{let i=s.role==="assistant"?"assistant":"user";this.#s(i==="user"?"processing":"speaking"),this.#J(i,s.text??"",!0);break}case"audio_end":this.#o?this.#o.markStreamEnd():(this.#d=!1,this.#s("listening"));break;case"metrics":this.#ve({llmMs:s.llm_ms,ttsMs:s.tts_ms,firstAudioMs:s.first_audio_ms,totalMs:s.total_ms});break;case"error":this.#R(new Error(s.error||"Voice error")),this.#s("error");break;case"session_end":this.#j={reason:String(s.reason),...typeof s.message=="string"&&{message:s.message}};break}}async#fe(){let e=this.bridge?.getHistory()??[],t="";try{let s=this.config?.callContext;t=String((typeof s=="function"?await s():s)??"")}catch{}return Ke(e,t)}#ie(e){let t=this.#L;if(!t)return;this.#L=null;let s=!1,i=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.#q={done:i,drop:()=>s=!0}}async#ne(){let e=this.#q;if(!e)return;let t,s=await Promise.race([e.done.then(()=>!1),new Promise(i=>{t=setTimeout(()=>i(!0),qe)})]);clearTimeout(t),s&&e.drop()}#re(e){this.#v=!0,this.#W=e}#oe(e,t=!1){this.#x.add(e),this.#W===e&&(this.#v=!1),this.#I.delete(e),this.bridge?.dropDelegation?.(e,t),this.#E.get(e)?.abort()}#me(e,t,s){let i=this.bridge,r=({type:d,...a})=>{let o=this.#e;return s!==this.#t||o?.readyState!==WebSocket.OPEN||this.#x.has(e)?!1:(o.send(JSON.stringify({type:d,delegationId:e,...a})),!0)},l=(d,a,o,c=0)=>r({type:d,status:a,text:o,...c>0&&{streamedChars:c}});this.#$=this.#$.then(async()=>{if(s!==this.#t||this.#x.has(e))return;let d=null,a=this.#f.has("delegation_stream")&&this.#f.has("delegation_read_back"),o=b=>{d=(d??this.#ne()).then(()=>{r({type:"delegation_delta",text:b})&&this.#T.add(e)})},c=await i.runDelegatedTurn(a?{...t,stream:o}:t).catch(()=>({status:"failed",text:""}));await this.#ne(),await d;let u=a?c.streamedChars??0:0,p=!!c.followUp,m=p&&this.#f.has("delegation_update");if(!l(m?"delegation_update":"delegation_result",p&&!m?"completed":c.status,c.text,u)||(c.status!=="failed"&&c.inChat!==!1&&this.#T.add(e),!c.followUp))return;let f=new AbortController;this.#E.set(e,f),c.followUp({signal:f.signal,approvalTimeoutMs:this.config?.approvalTimeoutMs&&Math.min(this.config.approvalTimeoutMs,We),readBack:m,onUpdate:b=>l("delegation_update","pending_approval",b)}).then(b=>{this.#E.delete(e),m&&b&&!f.signal.aborted&&l("delegation_result",b.status,b.text)})})}#ye(e,t){if(t!==this.#t||!this.#o||this.#K())return;let s=Ye(e);s.length!==0&&(this.#d||(this.#d=!0,this.#s("speaking")),this.#Y(),this.#o.enqueue(s),this.#u&&this.#be(s.length))}#K(){return Date.now()<this.#h}#Y(){if(this.#A||!this.#o)return;this.#A=!0;let e=this.#t;this.#o.onFinished(()=>{e===this.#t&&(this.#A=!1,this.#ae())})}#be(e){let t=Date.now();this.#w=Math.max(t,this.#w)+e/48,clearTimeout(this.#S),this.#S=setTimeout(()=>this.#ae(),this.#w-t+De)}#ae(){this.#d=!1,this.#w=0,this.#e&&this.#e.readyState===WebSocket.OPEN&&this.#s(this.#g?"processing":"listening")}#ce(){this.#o?.flush(),this.#A=!1,this.#Y(),this.#d=!1,this.#w=0,clearTimeout(this.#S)}#M(){this.#t+=1,this.#p=!1,this.#d=!1,this.#N(),this.#u=!1,this.#g=!1,this.#k=!1,this.#f=new Set;for(let e of this.#E.values())e.abort();if(this.#E.clear(),this.#x.clear(),this.#O=void 0,this.#j=null,clearTimeout(this.#Z),this.#B=!1,this.#L=null,this.#q=null,this.#$=Promise.resolve(),this.#T.clear(),this.#I.clear(),this.#W=null,this.#v=!1,this.#P.clear(),this.#V.clear(),this.#A=!1,this.#h=0,this.#w=0,clearTimeout(this.#S),this.#m=[],this.#y=0,this.#l&&(this.#l.onaudioprocess=null,this.#l.disconnect(),this.#l=null),this.#c&&(this.#c.disconnect(),this.#c=null),this.#n&&(this.#n.getTracks().forEach(e=>e.stop()),this.#n=null),this.#i&&(this.#i.close().catch(()=>{}),this.#i=null),this.#o&&(this.#o.destroy(),this.#o=null),this.#e){this.#b=!0;try{this.#e.close(1e3,"client ended call")}catch{}this.#e=null}}onResult(e){this.#Q.push(e)}onError(e){this.#H.push(e)}onStatusChange(e){this.#F.push(e)}onTranscript(e){this.#z.push(e)}onMetrics(e){this.#X.push(e)}#s(e){this.#F.forEach(t=>t(e))}#R(e){this.#H.forEach(t=>t(e))}#J(e,t,s,i){this.#z.forEach(r=>r(e,t,s,i))}#ve(e){this.#X.forEach(t=>t(e))}};var _=class{constructor(e={}){this.config=e;this.type="browser";this.#e=null;this.#i=[];this.#r=[];this.#n=[];this.#c=!1;this.#l=typeof window<"u"?window:void 0}#e;#i;#r;#n;#c;#l;async connect(){this.#n.forEach(e=>e("connected"))}async startListening(){try{if(this.#c)throw new Error("Already listening");if(!this.#l)throw new Error("Window object not available");let e=this.#l.SpeechRecognition||this.#l.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(""),i=t.results[t.results.length-1].isFinal;this.#i.forEach(r=>r({text:s,confidence:i?.8:.5,provider:"browser"})),i&&!this.config?.continuous&&this.stopListening()},this.#e.onerror=t=>{this.#r.forEach(s=>s(new Error(t.error))),this.#n.forEach(s=>s("error"))},this.#e.onstart=()=>{this.#c=!0,this.#n.forEach(t=>t("listening"))},this.#e.onend=()=>{this.#c=!1,this.#n.forEach(t=>t("idle"))},this.#e.start()}catch(e){throw this.#r.forEach(t=>t(e)),this.#n.forEach(t=>t("error")),e}}async stopListening(){this.#e&&(this.#e.stop(),this.#e=null),this.#c=!1,this.#n.forEach(e=>e("idle"))}onResult(e){this.#i.push(e)}onError(e){this.#r.push(e)}onStatusChange(e){this.#n.push(e)}async disconnect(){await this.stopListening(),this.#n.forEach(e=>e("disconnected"))}static isSupported(){return"SpeechRecognition"in window||"webkitSpeechRecognition"in window}};function O(n){switch(n.type){case"runtype":if(!n.runtype)throw new Error("Runtype voice provider requires configuration");return new V(n.runtype);case"browser":if(!_.isSupported())throw new Error("Browser speech recognition not supported");return new _(n.browser||{});case"custom":{let e=n.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: ${n.type}`)}}function pe(n){if(n?.type==="custom"&&n.custom)return O({type:"custom",custom:n.custom});if(n?.type==="runtype"&&n.runtype)return O({type:"runtype",runtype:n.runtype});if(_.isSupported())return O({type:"browser",browser:n?.browser||{language:"en-US"}});throw new Error("No supported voice providers available")}function Qe(n){try{return pe(n),!0}catch{return!1}}var he=n=>n.toLowerCase().replace(/[^\p{L}\p{N}]+/gu,""),J=class{constructor(e){this.host=e;this.turns=new Map;this.latestId=null;this.cancelled=new Set}apply(e,t,s,i,r,l){let d=this.host;t=t.trim();let a=this.turns.get(i);if(a||(a={},this.turns.set(i,a),this.latestId=i),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(i)){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 i;if(s){if(i=this.turns.get(s),i||(i={},this.turns.set(s,i)),i.claimed)return null;(!i.userId||!this.host.find(i.userId))&&(i.userFinal=!0,i.userId=this.host.inject({role:"user",content:e.trim()}).id)}else{let r=he(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:he(a.content)}]:[]}).filter(({text:d})=>d&&r);i=(l.find(({text:d})=>d===r)??l.find(({text:d})=>d.startsWith(r)||r.startsWith(d)))?.turn}return i?(i.claimed=!0,i.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 i=this.host.inject({role:"assistant",content:e,streaming:!1,voiceProcessing:!1}),r=t?this.turns.get(t):void 0;r&&(r.assistantId=i.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[i,r]of[[s.userId,s.userStartMs],[s.assistantId,s.assistantStartMs]])i&&r!==void 0&&r>e&&(!t||r<t.start)&&(t={start:r,id:i});return t&&this.host.find(t.id)}placeBefore(e){if(!e)return;let t=e.sequence??0,s=t-1;for(let i of this.turns.values())for(let r of[i.userId,i.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 i=this.turns.get(s)?.assistantId;i&&(t.add(i),this.host.markSpoken(i))}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 et=new Set(["mr","mrs","ms","dr","prof","sr","jr","st","mt","vs","etc","approx","inc","ltd","co","corp","no","nos","fig","ft","dept","est","min","max","e.g","i.e","a.m","p.m","u.s","u.k"]),tt=/([.!?]+|[,;:])(["'”’)\]*_]*)(\s+)|\n\s*/g;function st(n){let e=/(\S+)$/.exec(n)?.[1];if(!e)return!1;let t=e.replace(/^[("'“‘[*_]+/,"").toLowerCase();if(et.has(t)||/^[a-z](?:\.[a-z])*$/.test(t))return!0;if(/^\d+$/.test(e)){let s=n.length-e.length;return s===0||/\n\s*$/.test(n.slice(0,s))}return!1}function*ge(n,e){let t=new RegExp(tt.source,"g");for(let s=t.exec(n);s;s=t.exec(n)){let i=s[1];if(!n.slice(0,s.index+(i?.length??0)).trim()||i!==void 0&&(/^[,;:]$/.test(i)&&!e||i==="."&&st(n.slice(0,s.index))))continue;let r=s.index+s[0].length;r<n.length&&(yield r)}}function fe(n,e){let t=n.slice(0,e),s=0;for(let i of t.matchAll(/\s+/g))t.slice(0,i.index).trim()&&(s=i.index+i[0].length);return s}function Z(n){let e=0;for(let i of ge(n.slice(0,220),!0))e=i;if(e)return e;let t=fe(n,220);if(t)return t;let s=n.charCodeAt(219);return s>=55296&&s<=56319?219:220}var U=class{#e="";#i=!0;push(e){return this.#e+=e,this.#r()}flush(){let e=this.#r(),t=this.#e;if(this.#e="",t.trim()){for(let s=0;s<t.length;){let i=t.slice(s),r=i.length>220?Z(i):i.length;e.push(i.slice(0,r)),s+=r}this.#i=!1}return e}idle(){let e=this.#e;if(!e.trim()||!/(?:[.!?…]["'”’)\]*_]*|\n)\s*$/.test(e))return[];this.#e="",this.#i=!1;let t=[];for(let s=0;s<e.length;){let i=e.slice(s),r=i.length>220?Z(i):i.length;t.push(i.slice(0,r)),s+=r}return t}#r(){let e=[];for(let t=this.#n();t>0;t=this.#n())e.push(this.#e.slice(0,t)),this.#e=this.#e.slice(t),this.#i=!1;return e}#n(){let e=this.#e;if(!e.trim())return 0;let t=ge(e,this.#i).next().value??0;if(t&&t<=220)return t;let s=/[.!?]["'”’)\]*_]*$/.test(e);if(this.#i&&!s){let i=e.match(/\S+\s+/g)?.length??0;if(e.trim().length>=30||i>=6){let r=fe(e,220);if(r)return r}}return e.length>220?Z(e):0}};var it=200,nt=(n,e)=>{let t=0;for(;t<n.length&&t<e.length&&n[t]===e[t];)t+=1;return t},rt=50,ot="I need your answer in the chat before I can continue.",at=1e3,ct=5*6e4,me="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)",lt=new RegExp(`^(?:${me})(?: (?:${me}))?(?: thanks| thank you)?$`),dt=n=>lt.test(n.toLowerCase().replace(/[^a-z' ]+/g," ").replace(/\s+/g," ").trim()),ye="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.",be=()=>new Promise(n=>setTimeout(n,rt)),Q=(n,e)=>n.length>e?`${n.slice(0,e-1).trimEnd()}\u2026`:n,E=n=>n.replace(/^webmcp:/,"").replace(/([a-z\d])([A-Z])/g,"$1 $2").replace(/[_\-.]+/g," ").trim().toLowerCase();function ee(n,e=0){if(n==null||n==="")return"";if(typeof n!="object")return String(n);if(e>2)return"";if(Array.isArray(n))return n.map(r=>ee(r,e+1)).filter(Boolean).join(", ");let t=n,s=t.quantity??t.qty??t.count,i=t.name??t.item??t.product??t.title;return s!=null&&i!=null&&typeof i!="object"?`${s} ${i}`:Object.entries(t).filter(([r])=>!r.startsWith("_")).map(([r,l])=>{let d=ee(l,e+1);return d&&(e?`${E(r)} ${d}`:`${E(r)}: ${d}`)}).filter(Boolean).join(e?", ":"; ")}function ve(n){let e=n.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=at-e.length-ye.length-2,s=n.map(i=>{let r=[E(i.toolName)];i.description&&r.push(`(${Q(i.description.trim(),160)})`);let l=ee(i.parameters);return l&&r.push(`with ${l}`),i.reason&&r.push(`because: ${Q(i.reason.trim(),160)}`),Q(`- ${r.join(" ")}`,Math.floor(t/n.length)-1)});return`${e}
|
|
6
6
|
${s.join(`
|
|
7
7
|
`)}
|
|
8
8
|
|
|
9
|
-
${
|
|
9
|
+
${ye}`}var D=n=>n.variant==="approval"&&n.approval?.status==="pending";function ut(n){let e=null,t=null,s=()=>n.track(e??t),i=[],r=new Map,l=new Map,d=(g,f)=>{r.set(g,[...r.get(g)??[],f]),r.size>32&&r.delete(r.keys().next().value)},a=g=>n.messages().find(f=>f.id===g),o=g=>g.approvals.filter(f=>a(f)?.approval?.status==="pending"),c=g=>E(a(g)?.approval?.toolName??""),u=(g,f,b)=>{g.outcome={status:f,text:b};for(let v of o(g))n.decide(v)},p=g=>g.flatMap(f=>n.messages().find(b=>b.id===f)??[]).filter(f=>!f.variant&&f.content.trim()).map(f=>f.content.trim()).join(`
|
|
10
10
|
|
|
11
|
-
`),m=async
|
|
11
|
+
`),m=async g=>{do await be();while(n.busy()&&!(g&&(n.parked()||g.failed)))};return{getHistory:()=>{let g=n.messages().filter(v=>(v.role==="user"||v.role==="assistant")&&!v.variant&&!v.voiceProcessing&&!v.streaming&&v.content),f=!1,b=[];for(let v=g.length-1;v>=0;v-=1){let C=g[v];if(C.role==="user")f=!1;else if(!C.voiceCaption)f=!0;else if(f)continue;b.unshift({role:C.role,content:C.content})}return b},dropDelegation(g,f){l.set(g,!!f),l.size>32&&l.delete(l.keys().next().value);let b=f&&i.find(v=>v.delegationId===g);b&&!b.outcome&&u(b,"timeout","");for(let v of r.get(g)??[])v.stopStream?.(),v.dropped=!0,v.ids.length&&n.unspoken(v.ids)},async runDelegatedTurn({delegationId:g,userText:f,userUtteranceIds:b,stream:v}){let C=i.filter(h=>o(h).length),B=C.flatMap(o);if(B.length===1&&dt(f)){let h=c(B[0]);return u(C[0],"denied",""),{status:"denied",text:`Okay, I cancelled the ${h} request. Nothing was done.`,inChat:!1}}let te=n.parked()&&n.messages().every(h=>!D(h)||B.includes(h.id));n.busy()&&!te&&await m();let we=new Set(n.messages().map(h=>h.id)),q=()=>n.messages().filter(h=>!we.has(h.id)&&h.variant==="approval"),S={ids:[],failed:!1},x=v?new U:null,R="",se="",I=!1,$=h=>{for(let y of h)se+=y,v(y)},P;x&&(S.onUpdate=()=>{if(I)return;let h=p(S.ids);if(!h.startsWith(R)){I=!0,clearTimeout(P);return}if(h.length===R.length)return;let y=h.slice(R.length);R=h,$(x.push(y)),clearTimeout(P),P=setTimeout(()=>{I||$(x.idle())},it)},S.stopStream=()=>{clearTimeout(P),I=!0,S.onUpdate=void 0}),d(g,S),e=S,s();try{await n.send(f,n.claim(f,b)??void 0),await m(S)}catch{S.failed=!0}finally{e=null,s()}x&&(S.onUpdate?.(),clearTimeout(P),I||$(x.flush()),S.onUpdate=void 0,S.stopStream=void 0);let W=h=>{let y=nt(se,h);return y>0?{streamedChars:y}:{}},Se=S.ids.flatMap(h=>n.messages().find(y=>y.id===h)??[]),k=p(S.ids),j=S.failed?[]:q().filter(D),Ae=new Set(j.map(h=>E(h.approval.toolName)));for(let h of C){if(h.outcome)continue;for(let A of o(h).filter(H=>Ae.has(c(H))))h.replaced.add(A),n.decide(A);let y=c(h.approvals[0]);h.approvals.every(A=>h.replaced.has(A))?h.outcome={status:"cancelled",text:`The earlier ${y} request was replaced by the new one; it was not done.`}:te&&h.approvals.every(A=>a(A)?.approval?.status==="denied")&&(h.outcome={status:"cancelled",text:`The earlier ${y} request was replaced by the new one; it was not done.`})}if(j.length){let h=new Set(j.map(A=>A.id)),y={delegationId:g,approvals:[...h],at:Date.now(),replaced:new Set};return i.push(y),l.get(g)&&u(y,"timeout",""),k=`${k}
|
|
12
12
|
|
|
13
|
-
${
|
|
13
|
+
${ve(j.map(A=>A.approval))}`.trim(),{status:"pending_approval",text:k,...W(k),followUp:async({signal:A,approvalTimeoutMs:H=ct,readBack:ie=!0,onUpdate:ke})=>{let M={ids:[],failed:!1};r.delete(g),d(g,M);let ne=()=>q().filter(w=>h.has(w.id)||M.ids.includes(w.id)),Ce=()=>q().filter(w=>D(w)&&!h.has(w.id)&&!i.some(T=>T.approvals.includes(w.id))&&(M.ids.includes(w.id)||w.approval?.toolType==="webmcp"&&t===M)),re=()=>{t=M,s()};ie&&re();try{for(;!A.aborted&&!y.outcome&&(e||n.busy()||ne().some(D));){ie&&t!==M&&!o(y).length&&re();let w=Ce();if(w.length){for(let T of w)h.add(T.id),y.approvals.push(T.id);y.at=Date.now(),ke?.(ve(w.map(T=>T.approval)))}if(Date.now()-y.at>=H&&o(y).length&&(!n.busy()||n.parked())){u(y,"timeout",`That ${c(y.approvals[0])} request expired, so nothing was done.`);break}await be()}}finally{t===M&&(t=null,s()),i.splice(i.indexOf(y),1)}if(A.aborted)return null;if(y.outcome)return y.outcome;if(M.failed)return{status:"failed",text:`The ${c(y.approvals[0])} request didn't complete.`};let oe=ne(),F=w=>oe.filter(w).map(T=>E(T.approval.toolName)).join(", "),z=oe.some(w=>w.approval?.status==="approved"),X=F(w=>w.approval?.status==="denied"&&!y.replaced.has(w.id)),G=F(w=>y.replaced.has(w.id)),K=F(w=>w.approval?.status==="timeout"),Me=[z?p(M.ids):"",X&&`The user declined the ${X} request in the chat, so ${z?"that part was not done":"nothing was done"}.`,G&&`The earlier ${G} request was replaced by the new one; it was not done.`,K&&`The ${K} request timed out, so it was not done.`].filter(Boolean).join(`
|
|
14
14
|
|
|
15
|
-
`);return{status:
|
|
15
|
+
`);return{status:z?"completed":X?"denied":G?"cancelled":K?"timeout":"completed",text:Me}}}}return!S.failed&&n.parked()||Se.some(h=>h.agentMetadata?.awaitingLocalTool&&!h.agentMetadata.askUserQuestionAnswered&&!h.toolCall?.name?.startsWith("webmcp:"))?(k=`${k}
|
|
16
16
|
|
|
17
|
-
${
|
|
17
|
+
${ot}`.trim(),{status:S.failed?"failed":"completed",text:k,...W(k)}):{status:!S.failed&&k?"completed":"failed",text:k,...W(k)}}}}var pt=(n,e,t,s)=>{let i=n.session;try{n.keyed=t?new t({find:o=>i.messages.find(c=>c.id===o),inject:o=>i.injectMessage(o),upsert:o=>i.upsertMessage(o),settle:o=>{i.messages=i.messages.map(c=>o.has(c.id)?{...c,streaming:!1,voiceProcessing:!1}:c),i.callbacks.onMessagesChanged([...i.messages])},setStreaming:o=>{(o||!n.busy())&&n.setStreaming(o)},markSpoken:o=>{i.ttsSpokenMessageIds.add(o)}}):null,s&&e.setSessionBridge&&e.setSessionBridge(s({messages:()=>i.messages,busy:()=>n.busy(),parked:()=>i.webMcpApprovalResolvers.size>0,claim:(o,c)=>n.keyed?.claimUserTurn(o,c)??null,send:(o,c)=>i.sendMessage(o,{viaVoice:!0,voiceTurn:{userMessageId:c}}),track:o=>{n.setDelegation(o)},unspoken:o=>{for(let c of o)i.ttsSpokenMessageIds.delete(c);i.streaming||i.speakLatestAssistantMessage(o)},decide:o=>{if(i.webMcpApprovalResolvers.has(o))return i.resolveWebMcpApproval(o,"denied");let c=i.messages.find(u=>u.id===o)?.approval;c?.status==="pending"&&i.resolveApproval(c,"denied")}}));let r=n.generation(),l=()=>n.provider()===e&&r===n.generation(),a=(i.config.voiceRecognition??{}).processingErrorText??"Voice processing failed. Please try again.";e.onResult(o=>{l()&&o.provider!=="runtype"&&o.text&&o.text.trim()&&i.sendMessage(o.text,{viaVoice:!0})}),e.onTranscript&&e.onTranscript((o,c,u,p)=>{if(l()){if(p?.turnId){n.keyed?.apply(o,c,u,p.turnId,p.startMs,p.caption);return}if(o==="user"){if(n.pendingUser)i.upsertMessage({id:n.pendingUser,role:"user",content:c,createdAt:new Date().toISOString(),streaming:!1,voiceProcessing:!u});else{let m=i.injectMessage({role:"user",content:c,streaming:!1,voiceProcessing:!u});n.pendingUser=m.id}if(u){n.turns().start(p?.turnId),n.pendingUser=null;let m=i.injectMessage({role:"assistant",content:"",streaming:!0,voiceProcessing:!0});n.pendingAssistant=m.id,n.setStreaming(!0)}}else{if(!n.turns().accepts(p?.turnId))return;if(n.pendingAssistant)i.upsertMessage({id:n.pendingAssistant,role:"assistant",content:c,createdAt:new Date().toISOString(),streaming:!u,voiceProcessing:!u});else{let m=i.injectMessage({role:"assistant",content:c,streaming:!u,voiceProcessing:!u});n.pendingAssistant=m.id}u&&(n.pendingAssistant&&i.ttsSpokenMessageIds.add(n.pendingAssistant),n.setStreaming(!1),n.pendingAssistant=null)}}}),e.onLevel&&e.onLevel(o=>{l()&&n.setLevel(Number.isFinite(o)?Math.max(0,Math.min(1,o)):0)}),e.onMetrics&&e.onMetrics(o=>{l()&&i.config.voiceRecognition?.onMetrics?.(o)}),e.onError(o=>{l()&&(console.error("Voice error:",o),n.pendingAssistant&&(i.upsertMessage({id:n.pendingAssistant,role:"assistant",content:a,createdAt:new Date().toISOString(),streaming:!1,voiceProcessing:!1}),n.setStreaming(!1),n.pendingUser=null,n.pendingAssistant=null),n.keyed?.fail(a))}),e.onStatusChange(o=>{l()&&(n.setStatus(o),(o==="listening"||o==="idle"||o==="disconnected")&&n.settlePending(o!=="listening"),(o==="idle"||o==="disconnected")&&n.keyed?.settle(),i.callbacks.onVoiceStatusChanged?.(o))}),e.connect()}catch(r){console.error("Failed to setup voice:",r)}};var ht=n=>{let e=a=>{if(n.original)return;let o=n.config().voiceRecognition??{};n.original={backgroundColor:a.style.backgroundColor,color:a.style.color,borderColor:a.style.borderColor,iconName:o.iconName??"mic",iconSize:parseFloat(o.iconSize??"")||n.iconFallbackPx}},t=(a,o,c)=>{let u=a.querySelector("svg");u&&u.remove();let p=n.original?.iconSize??(parseFloat(n.config().voiceRecognition?.iconSize??"")||n.iconFallbackPx),m=n.renderIcon(o,p,c,1.5);m&&a.appendChild(m)},s=a=>{a.classList.remove("persona-voice-recording","persona-voice-processing","persona-voice-speaking")};return{recording:()=>{let a=n.mic();if(!a)return;e(a);let o=n.config().voiceRecognition??{},c=o.recordingBackgroundColor,u=o.recordingIconColor,p=o.recordingBorderColor;if(s(a),a.classList.add("persona-voice-recording"),n.setMicState("recording"),a.style.backgroundColor=c??"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=n.mic();if(!a)return;e(a);let o=n.config().voiceRecognition??{},c=n.session().getVoiceInterruptionMode(),u=n.original,p=o.processingIconName??"loader",m=o.processingIconColor??u?.color??"",g=o.processingBackgroundColor??u?.backgroundColor??"",f=o.processingBorderColor??u?.borderColor??"";s(a),a.classList.add("persona-voice-processing"),n.setMicState("processing"),a.style.backgroundColor=g,a.style.borderColor=f;let b=m||"currentColor";a.style.color=b,t(a,p,b),a.setAttribute("aria-label","Processing voice input"),c==="none"&&(a.style.cursor="default")},speaking:()=>{let a=n.mic();if(!a)return;e(a);let o=n.config().voiceRecognition??{},c=n.session().getVoiceInterruptionMode(),u=n.original,p=c==="cancel"?"square":c==="barge-in"?"mic":"volume-2",m=o.speakingIconName??p,g=o.speakingIconColor??(c==="barge-in"?o.recordingIconColor??u?.color??"":u?.color??""),f=o.speakingBackgroundColor??(c==="barge-in"?o.recordingBackgroundColor??"var(--persona-voice-recording-bg, #ef4444)":u?.backgroundColor??""),b=o.speakingBorderColor??(c==="barge-in"?o.recordingBorderColor??"":u?.borderColor??"");s(a),a.classList.add("persona-voice-speaking"),n.setMicState("speaking"),a.style.backgroundColor=f,a.style.borderColor=b;let v=g||"currentColor";a.style.color=v,t(a,m,v);let C=c==="cancel"?"Stop playback and re-record":c==="barge-in"?"Speak to interrupt":"Agent is speaking";a.setAttribute("aria-label",C),c==="none"&&(a.style.cursor="default"),c==="barge-in"&&a.classList.add("persona-voice-recording")},reset:()=>{let a=n.mic();if(!a)return;s(a),n.setMicState("idle");let o=n.original;o&&(a.style.backgroundColor=o.backgroundColor??"",a.style.color=o.color??"",a.style.borderColor=o.borderColor??"",t(a,o.iconName,o.color||"currentColor"),n.original=null),a.style.cursor="",a.setAttribute("aria-label","Start voice recognition")}}};export{_ as BrowserVoiceProvider,J as KeyedVoiceTranscript,V as RuntypeVoiceProvider,pe as createBestAvailableVoiceProvider,ht as createMicStateStyles,O as createVoiceProvider,ut as createVoiceSessionBridge,Qe as isVoiceSupported,pt as wireSessionVoice};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@runtypelabs/persona",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.28.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",
|
|
@@ -90,6 +90,24 @@ describe("client-token turn identity", () => {
|
|
|
90
90
|
expect(bodies[1].turnId).not.toBe(bodies[0].turnId);
|
|
91
91
|
});
|
|
92
92
|
|
|
93
|
+
it("a voice-spoken send carries the spoken-reply hint; others don't", async () => {
|
|
94
|
+
const bodies: Record<string, unknown>[] = [];
|
|
95
|
+
global.fetch = vi.fn(async (url: unknown, init: any) => {
|
|
96
|
+
if (String(url).endsWith("/v1/client/init")) return initResponse();
|
|
97
|
+
bodies.push(JSON.parse(init.body));
|
|
98
|
+
const stream = controlledStream();
|
|
99
|
+
stream.close();
|
|
100
|
+
return stream.response;
|
|
101
|
+
}) as unknown as typeof fetch;
|
|
102
|
+
|
|
103
|
+
const client = clientTokenClient();
|
|
104
|
+
await client.dispatch({ messages, voiceSpoken: true }, () => {});
|
|
105
|
+
await client.dispatch({ messages }, () => {});
|
|
106
|
+
|
|
107
|
+
expect(bodies[0].voice).toEqual({ spoken: true });
|
|
108
|
+
expect(bodies[1]).not.toHaveProperty("voice");
|
|
109
|
+
});
|
|
110
|
+
|
|
93
111
|
it("drops SSE events belonging to a superseded turn", async () => {
|
|
94
112
|
const first = controlledStream();
|
|
95
113
|
const second = controlledStream();
|
package/src/client.ts
CHANGED
|
@@ -74,6 +74,12 @@ type DispatchOptions = {
|
|
|
74
74
|
* `submitMode: "interrupt"` so the server cancels the prior run.
|
|
75
75
|
*/
|
|
76
76
|
interrupt?: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Client-token only: a voice model reads the reply aloud (a delegated voice
|
|
79
|
+
* turn). Sends `voice: { spoken: true }` so the server asks for a speakable
|
|
80
|
+
* answer.
|
|
81
|
+
*/
|
|
82
|
+
voiceSpoken?: boolean;
|
|
77
83
|
};
|
|
78
84
|
|
|
79
85
|
export type SSEHandler = (event: AgentWidgetEvent) => void;
|
|
@@ -1510,6 +1516,7 @@ export class AgentWidgetClient {
|
|
|
1510
1516
|
// superseded run and this client can drop its stale events below.
|
|
1511
1517
|
turnId,
|
|
1512
1518
|
...(options.interrupt && { submitMode: 'interrupt' as const }),
|
|
1519
|
+
...(options.voiceSpoken && { voice: { spoken: true as const } }),
|
|
1513
1520
|
};
|
|
1514
1521
|
|
|
1515
1522
|
// Diff-only / send-once WebMCP tool dispatch. `buildPayload()` already
|
package/src/session.ts
CHANGED
|
@@ -2042,7 +2042,7 @@ export class AgentWidgetSession {
|
|
|
2042
2042
|
controller: AbortController,
|
|
2043
2043
|
assistantMessageId: string,
|
|
2044
2044
|
userMessageId: string,
|
|
2045
|
-
turnOptions?: { composerOptions?: ComposerOptionsPayload; interrupt?: boolean }
|
|
2045
|
+
turnOptions?: { composerOptions?: ComposerOptionsPayload; interrupt?: boolean; voiceSpoken?: boolean }
|
|
2046
2046
|
): Promise<void> {
|
|
2047
2047
|
this.#recordDispatchedProjections(snapshot);
|
|
2048
2048
|
try {
|
|
@@ -2052,7 +2052,8 @@ export class AgentWidgetSession {
|
|
|
2052
2052
|
signal: controller.signal,
|
|
2053
2053
|
assistantMessageId,
|
|
2054
2054
|
composerOptions: turnOptions?.composerOptions,
|
|
2055
|
-
interrupt: turnOptions?.interrupt
|
|
2055
|
+
interrupt: turnOptions?.interrupt,
|
|
2056
|
+
voiceSpoken: turnOptions?.voiceSpoken
|
|
2056
2057
|
},
|
|
2057
2058
|
this.handleEvent
|
|
2058
2059
|
);
|
|
@@ -2070,6 +2071,7 @@ export class AgentWidgetSession {
|
|
|
2070
2071
|
signal: controller.signal,
|
|
2071
2072
|
assistantMessageId,
|
|
2072
2073
|
composerOptions: turnOptions?.composerOptions,
|
|
2074
|
+
voiceSpoken: turnOptions?.voiceSpoken,
|
|
2073
2075
|
},
|
|
2074
2076
|
this.handleEvent
|
|
2075
2077
|
);
|
|
@@ -2705,7 +2707,9 @@ export class AgentWidgetSession {
|
|
|
2705
2707
|
userMessageId,
|
|
2706
2708
|
{
|
|
2707
2709
|
composerOptions: options?.composerOptions,
|
|
2708
|
-
interrupt: options?.interrupt
|
|
2710
|
+
interrupt: options?.interrupt,
|
|
2711
|
+
// The delegated voice turn's reply is read aloud (spoken-reply hint).
|
|
2712
|
+
voiceSpoken: !!options?.voiceTurn
|
|
2709
2713
|
}
|
|
2710
2714
|
);
|
|
2711
2715
|
} catch (error) {
|
|
@@ -3679,6 +3683,7 @@ export class AgentWidgetSession {
|
|
|
3679
3683
|
|
|
3680
3684
|
private handleEvent = (event: AgentWidgetEvent) => {
|
|
3681
3685
|
const delegation = this.#voiceDelegation;
|
|
3686
|
+
let delegated = false;
|
|
3682
3687
|
if (delegation) {
|
|
3683
3688
|
if (event.type === "error" || (event.type === "status" && event.status === "error")) {
|
|
3684
3689
|
delegation.failed = true;
|
|
@@ -3687,10 +3692,13 @@ export class AgentWidgetSession {
|
|
|
3687
3692
|
// unless the server dropped the result (then nobody speaks it).
|
|
3688
3693
|
if (!delegation.dropped) this.ttsSpokenMessageIds.add(event.message.id);
|
|
3689
3694
|
if (!delegation.ids.includes(event.message.id)) delegation.ids.push(event.message.id);
|
|
3695
|
+
delegated = true;
|
|
3690
3696
|
}
|
|
3691
3697
|
}
|
|
3692
3698
|
if (event.type === "message") {
|
|
3693
3699
|
this.upsertMessage(event.message);
|
|
3700
|
+
// The delegated answer grew: a streaming voice bridge reads it now.
|
|
3701
|
+
if (delegated) delegation!.onUpdate?.();
|
|
3694
3702
|
|
|
3695
3703
|
// A resolved approval reaches this handler only from the server's
|
|
3696
3704
|
// `approval_complete` (resolveApproval's optimistic update upserts
|
|
@@ -781,6 +781,146 @@ describe('AgentWidgetSession - voice client delegation bridge', () => {
|
|
|
781
781
|
expect(result.followUp).toBeTypeOf('function');
|
|
782
782
|
});
|
|
783
783
|
|
|
784
|
+
describe('streaming the answer (delegation_stream)', () => {
|
|
785
|
+
/** Streams `chunks` into one assistant message, growing it like a text stream. */
|
|
786
|
+
const streamReply = (onEvent: (event: AgentWidgetEvent) => void, chunks: string[], id = 'assistant-s1') => {
|
|
787
|
+
const base = { id, role: 'assistant' as const, createdAt: new Date().toISOString() };
|
|
788
|
+
let content = '';
|
|
789
|
+
onEvent({ type: 'status', status: 'connecting' });
|
|
790
|
+
for (const chunk of chunks) {
|
|
791
|
+
content += chunk;
|
|
792
|
+
onEvent({ type: 'message', message: { ...base, content, streaming: true } });
|
|
793
|
+
}
|
|
794
|
+
onEvent({ type: 'message', message: { ...base, content, streaming: false } });
|
|
795
|
+
};
|
|
796
|
+
|
|
797
|
+
it('streams the answer in clause pieces while it renders, then reports streamedChars', async () => {
|
|
798
|
+
drive('user', 'what are your hours', true, 'u1');
|
|
799
|
+
const pieces: string[] = [];
|
|
800
|
+
const seenBeforeEnd: string[] = [];
|
|
801
|
+
dispatch.mockImplementation(async (_options, onEvent) => {
|
|
802
|
+
streamReply(onEvent, ['We open at ', 'eight, and close ', 'at six. On Sundays ', 'we are closed.']);
|
|
803
|
+
seenBeforeEnd.push(...pieces);
|
|
804
|
+
// A tool bubble between two answer messages never streams.
|
|
805
|
+
onEvent({
|
|
806
|
+
type: 'message',
|
|
807
|
+
message: { id: 'tool-1', role: 'assistant', content: '', createdAt: new Date().toISOString(), variant: 'tool' },
|
|
808
|
+
});
|
|
809
|
+
streamReply(onEvent, ['Anything else?'], 'assistant-s2');
|
|
810
|
+
onEvent({ type: 'status', status: 'idle' });
|
|
811
|
+
});
|
|
812
|
+
const result = await h.state.bridge!.runDelegatedTurn(
|
|
813
|
+
req({ delegationId: 'd1', userText: 'what are your hours', stream: (text) => pieces.push(text) }),
|
|
814
|
+
);
|
|
815
|
+
expect(result.text).toBe('We open at eight, and close at six. On Sundays we are closed.\n\nAnything else?');
|
|
816
|
+
// The first pieces went out while the stream was still running.
|
|
817
|
+
expect(seenBeforeEnd).toEqual(['We open at eight, ', 'and close at six. ']);
|
|
818
|
+
expect(pieces.join('')).toBe(result.text);
|
|
819
|
+
expect(result.streamedChars).toBe(result.text.length);
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
it('streams the answer of an approval-parked turn; the approval script is the unstreamed suffix', async () => {
|
|
823
|
+
drive('user', 'order two croissants', true, 'u1');
|
|
824
|
+
parkOnApproval();
|
|
825
|
+
const pieces: string[] = [];
|
|
826
|
+
const result = await h.state.bridge!.runDelegatedTurn(
|
|
827
|
+
req({ delegationId: 'd1', userText: 'order two croissants', stream: (text) => pieces.push(text) }),
|
|
828
|
+
);
|
|
829
|
+
expect(result.status).toBe('pending_approval');
|
|
830
|
+
expect(pieces).toEqual(['I can do that.']);
|
|
831
|
+
expect(result.streamedChars).toBe('I can do that.'.length);
|
|
832
|
+
expect(result.text.startsWith('I can do that.\n\n')).toBe(true);
|
|
833
|
+
expect(result.text.slice(result.streamedChars)).toContain("This action needs the user's approval");
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
it('stops streaming when the answer is rewritten, and counts only the shared prefix', async () => {
|
|
837
|
+
drive('user', 'hours', true, 'u1');
|
|
838
|
+
const pieces: string[] = [];
|
|
839
|
+
dispatch.mockImplementation(async (_options, onEvent) => {
|
|
840
|
+
const base = { id: 'assistant-rw', role: 'assistant' as const, createdAt: new Date().toISOString() };
|
|
841
|
+
onEvent({ type: 'message', message: { ...base, content: 'We open at eight. Sun', streaming: true } });
|
|
842
|
+
onEvent({ type: 'message', message: { ...base, content: 'We open at 8am. Closed Sundays.', streaming: false } });
|
|
843
|
+
onEvent({ type: 'status', status: 'idle' });
|
|
844
|
+
});
|
|
845
|
+
const result = await h.state.bridge!.runDelegatedTurn(
|
|
846
|
+
req({ delegationId: 'd1', userText: 'hours', stream: (text) => pieces.push(text) }),
|
|
847
|
+
);
|
|
848
|
+
expect(pieces).toEqual(['We open at eight. ']);
|
|
849
|
+
expect(result.text).toBe('We open at 8am. Closed Sundays.');
|
|
850
|
+
expect(result.streamedChars).toBe('We open at '.length);
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
it('streams a held sentence end after 200 ms of quiet, and stops on cancellation', async () => {
|
|
854
|
+
vi.useFakeTimers();
|
|
855
|
+
try {
|
|
856
|
+
drive('user', 'hours', true, 'u1');
|
|
857
|
+
const pieces: string[] = [];
|
|
858
|
+
let resume!: () => void;
|
|
859
|
+
const paused = new Promise<void>((resolve) => (resume = resolve));
|
|
860
|
+
const base = { id: 'assistant-idle', role: 'assistant' as const, createdAt: new Date().toISOString() };
|
|
861
|
+
dispatch.mockImplementation(async (_options, onEvent) => {
|
|
862
|
+
onEvent({ type: 'message', message: { ...base, content: 'We open at 8 a.m.', streaming: true } });
|
|
863
|
+
await paused;
|
|
864
|
+
onEvent({ type: 'message', message: { ...base, content: 'We open at 8 a.m. Closed Sundays.', streaming: false } });
|
|
865
|
+
onEvent({ type: 'status', status: 'idle' });
|
|
866
|
+
});
|
|
867
|
+
const turn = h.state.bridge!.runDelegatedTurn(
|
|
868
|
+
req({ delegationId: 'd1', userText: 'hours', stream: (text) => pieces.push(text) }),
|
|
869
|
+
);
|
|
870
|
+
await vi.advanceTimersByTimeAsync(199);
|
|
871
|
+
expect(pieces).toEqual([]);
|
|
872
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
873
|
+
expect(pieces).toEqual(['We open at 8 a.m.']);
|
|
874
|
+
resume();
|
|
875
|
+
await vi.advanceTimersByTimeAsync(500);
|
|
876
|
+
const result = await turn;
|
|
877
|
+
expect(pieces).toEqual(['We open at 8 a.m.', ' Closed Sundays.']);
|
|
878
|
+
expect(pieces.join('')).toBe(result.text);
|
|
879
|
+
expect(result.streamedChars).toBe(result.text.length);
|
|
880
|
+
|
|
881
|
+
// Cancelled while a sentence end is held: the idle flush never fires.
|
|
882
|
+
const later: string[] = [];
|
|
883
|
+
let resume2!: () => void;
|
|
884
|
+
const paused2 = new Promise<void>((resolve) => (resume2 = resolve));
|
|
885
|
+
const base2 = { ...base, id: 'assistant-idle-2' };
|
|
886
|
+
dispatch.mockImplementation(async (_options, onEvent) => {
|
|
887
|
+
onEvent({ type: 'message', message: { ...base2, content: 'Sure thing.', streaming: true } });
|
|
888
|
+
await paused2;
|
|
889
|
+
onEvent({ type: 'message', message: { ...base2, content: 'Sure thing. Done.', streaming: false } });
|
|
890
|
+
onEvent({ type: 'status', status: 'idle' });
|
|
891
|
+
});
|
|
892
|
+
const turn2 = h.state.bridge!.runDelegatedTurn(
|
|
893
|
+
req({ delegationId: 'd2', userText: 'more', stream: (text) => later.push(text) }),
|
|
894
|
+
);
|
|
895
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
896
|
+
h.state.bridge!.dropDelegation!('d2');
|
|
897
|
+
await vi.advanceTimersByTimeAsync(300);
|
|
898
|
+
expect(later).toEqual([]);
|
|
899
|
+
resume2();
|
|
900
|
+
await vi.advanceTimersByTimeAsync(500);
|
|
901
|
+
await turn2;
|
|
902
|
+
expect(later).toEqual([]);
|
|
903
|
+
} finally {
|
|
904
|
+
vi.useRealTimers();
|
|
905
|
+
}
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
it('streams nothing and omits streamedChars without a stream callback', async () => {
|
|
909
|
+
drive('user', 'hours', true, 'u1');
|
|
910
|
+
dispatch.mockImplementation(async (_options, onEvent) => reply(onEvent, 'We open at nine. Sundays closed.'));
|
|
911
|
+
const result = await h.state.bridge!.runDelegatedTurn(req({ delegationId: 'd1', userText: 'hours' }));
|
|
912
|
+
expect(result).not.toHaveProperty('streamedChars');
|
|
913
|
+
});
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
it('marks only the delegated voice turn as spoken (voice: { spoken: true })', async () => {
|
|
917
|
+
dispatch.mockImplementation(async (_options, onEvent) => reply(onEvent, 'Nine to five.'));
|
|
918
|
+
await h.state.bridge!.runDelegatedTurn(req({ delegationId: 'd1', userText: 'what are your hours' }));
|
|
919
|
+
await session.sendMessage('typed question');
|
|
920
|
+
const options = dispatch.mock.calls.map(([o]) => (o as { voiceSpoken?: boolean }).voiceSpoken);
|
|
921
|
+
expect(options).toEqual([true, false]);
|
|
922
|
+
});
|
|
923
|
+
|
|
784
924
|
it('follows up with the answer once the visitor approves, kept off browser TTS', async () => {
|
|
785
925
|
drive('user', 'order two croissants', true, 'u1');
|
|
786
926
|
parkOnApproval();
|
package/src/types.ts
CHANGED
|
@@ -5265,6 +5265,13 @@ export type VoiceDelegationRequest = {
|
|
|
5265
5265
|
userUtteranceIds: string[];
|
|
5266
5266
|
/** The voice model's view of the conversation it delegated from. */
|
|
5267
5267
|
messages: Array<{ role: string; content: unknown }>;
|
|
5268
|
+
/**
|
|
5269
|
+
* Set when the server streams delegated answers (`delegation_stream`): called
|
|
5270
|
+
* with each speakable piece of the turn's answer, in order, while it renders.
|
|
5271
|
+
* The pieces, concatenated, are a prefix of the result's `text`; the result
|
|
5272
|
+
* says how much of it with `streamedChars`.
|
|
5273
|
+
*/
|
|
5274
|
+
stream?: (text: string) => void;
|
|
5268
5275
|
};
|
|
5269
5276
|
|
|
5270
5277
|
/**
|
|
@@ -5305,6 +5312,12 @@ export type VoiceDelegationResult = {
|
|
|
5305
5312
|
* @default true
|
|
5306
5313
|
*/
|
|
5307
5314
|
inChat?: boolean;
|
|
5315
|
+
/**
|
|
5316
|
+
* How many leading characters of `text` already went out through
|
|
5317
|
+
* `request.stream` (the rest, such as an approval script, is unstreamed).
|
|
5318
|
+
* Omitted or 0: nothing was streamed.
|
|
5319
|
+
*/
|
|
5320
|
+
streamedChars?: number;
|
|
5308
5321
|
/**
|
|
5309
5322
|
* Set when the turn parked on approvals (`text` then asks for the decision).
|
|
5310
5323
|
* Resolves with the terminal result once the visitor decides (or the
|
|
@@ -6266,6 +6279,12 @@ export type ClientChatRequest = {
|
|
|
6266
6279
|
turnId?: RuntypeClientChatRequest['turnId'];
|
|
6267
6280
|
/** Whether this turn should interrupt a prior in-flight response (from Core public OpenAPI). */
|
|
6268
6281
|
submitMode?: RuntypeClientChatRequest['submitMode'];
|
|
6282
|
+
/**
|
|
6283
|
+
* Spoken-reply hint, set on a delegated voice turn whose reply a voice model
|
|
6284
|
+
* reads aloud: the server asks the agent for a speakable answer (the direct
|
|
6285
|
+
* answer first, in plain sentences). Not persisted into history.
|
|
6286
|
+
*/
|
|
6287
|
+
voice?: { spoken: true };
|
|
6269
6288
|
};
|
|
6270
6289
|
|
|
6271
6290
|
/**
|