@mindstudio-ai/interface 0.1.14 → 0.1.15

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/voice.d.ts CHANGED
@@ -38,10 +38,11 @@ interface VoiceToolCallEvent {
38
38
  /** Milliseconds since epoch. */
39
39
  at: number;
40
40
  /**
41
- * The tool's return value — present on 'done' only for tools whose voice.md
42
- * block declares `forwardResult: true`, so the UI can render what the agent
43
- * just did (a citation, a record, a confirmation) in lockstep with speech.
44
- * Delivered only to this session's room; platform-guaranteed, no polling.
41
+ * The tool's return value — present on every 'done' (unless truncated), so
42
+ * the UI can render what the agent just did (a citation, a record, a
43
+ * confirmation) in lockstep with speech. Delivered only to this session's
44
+ * room — the same security context as the invocation. Failed calls carry
45
+ * no result.
45
46
  */
46
47
  result?: unknown;
47
48
  /** Set instead of `result` when the payload exceeded the forwarding size
@@ -80,9 +81,66 @@ interface VoiceSession {
80
81
  * that are painful aloud: addresses, codes, emails.
81
82
  */
82
83
  sendText(text: string): Promise<void>;
84
+ /**
85
+ * Register a handler for a client tool — a tool declared in voice.md with
86
+ * `target: "client"`, whose effect lives in this browser (open a sheet,
87
+ * navigate, highlight). When the agent invokes it, the handler runs and its
88
+ * return value goes back to the agent as the tool result — a real
89
+ * request/response, so the agent knows the action happened. Throwing (or an
90
+ * unregistered tool) returns an error the agent speaks around; the agent
91
+ * waits up to ~30s. One handler per tool name (later registrations
92
+ * replace); returns an unregister function.
93
+ */
94
+ registerClientTool(name: string, handler: (args: unknown) => unknown | Promise<unknown>): () => void;
95
+ /**
96
+ * Upgrade this live session from anonymous to the now-signed-in user —
97
+ * call it right after your in-app verification succeeds (progressive
98
+ * auth). Subsequent tool calls run as the verified user with their roles,
99
+ * and the agent's Current User context refreshes, WITHOUT tearing down
100
+ * the conversation. Requires the session to have been started anonymously
101
+ * (an already-identified session rejects with `already_identified`);
102
+ * the SDK proves ownership with the token the session was minted under.
103
+ * On failure, ending and restarting the session is the fallback.
104
+ */
105
+ refreshIdentity(): Promise<void>;
83
106
  /** End the session and release the microphone. */
84
107
  end(): Promise<void>;
85
108
  }
109
+ /**
110
+ * The active session's QA/debug handle, published on `window.__MS_VOICE__`
111
+ * while a session is live (cleared when it ends; last session wins).
112
+ *
113
+ * Exists so automation that can't produce audio — the platform's browser QA
114
+ * agent driving the app in headless Chrome — can converse with the voice
115
+ * agent by injected text and assert on what it said and which tools ran,
116
+ * while the app's real UI (client-tool cards, state orb, captions) renders
117
+ * normally. It grants nothing the page's own scripts don't already have:
118
+ * it's the same session object, in the same execution context, made
119
+ * reachable instead of trapped in the app's closure.
120
+ */
121
+ interface VoiceQaHandle {
122
+ sessionId: string;
123
+ /** Live session state (reads through to the session). */
124
+ readonly state: VoiceSessionState;
125
+ /** Inject a user turn — same as `session.sendText`. */
126
+ sendText(text: string): Promise<void>;
127
+ /**
128
+ * Caption history so far, one entry per segment (entries update in place as
129
+ * a segment grows; `final` marks settled ones). Read before `end()` — the
130
+ * handle is removed when the session ends.
131
+ */
132
+ transcript: VoiceTranscriptEvent[];
133
+ /** Tool activity so far, in arrival order (`done` entries carry `result`). */
134
+ toolCalls: VoiceToolCallEvent[];
135
+ /** End the session — same as `session.end`. */
136
+ end(): Promise<void>;
137
+ }
138
+ declare global {
139
+ interface Window {
140
+ /** Active voice session's QA/debug handle — see {@link VoiceQaHandle}. */
141
+ __MS_VOICE__?: VoiceQaHandle;
142
+ }
143
+ }
86
144
  /** A past voice session (call record), transcript excluded. */
87
145
  interface VoiceSessionSummary {
88
146
  id: string;
@@ -151,4 +209,4 @@ interface VoiceClient {
151
209
  */
152
210
  declare function createVoiceClient(): VoiceClient;
153
211
 
154
- export { type StartSessionOptions, type VoiceClient, type VoiceSession, type VoiceSessionCallbacks, type VoiceSessionDetail, type VoiceSessionListPage, type VoiceSessionState, type VoiceSessionSummary, type VoiceToolCallEvent, type VoiceTranscriptEntry, type VoiceTranscriptEvent, createVoiceClient };
212
+ export { type StartSessionOptions, type VoiceClient, type VoiceQaHandle, type VoiceSession, type VoiceSessionCallbacks, type VoiceSessionDetail, type VoiceSessionListPage, type VoiceSessionState, type VoiceSessionSummary, type VoiceToolCallEvent, type VoiceTranscriptEntry, type VoiceTranscriptEvent, createVoiceClient };
package/dist/voice.js CHANGED
@@ -1,2 +1,2 @@
1
- import{a as p,c as y}from"./chunk-5L3RRB4F.js";var k="/_/voice",v="lk.transcription",P="lk.chat",C="ms.voice.tool",A="lk.transcription_final",x="lk.segment_id",I="lk.agent.state";async function w(i,l,s){let a={Authorization:`Bearer ${y().token}`};s!==void 0&&(a["Content-Type"]="application/json");let r=await fetch(`${k}${i}`,{method:l,headers:a,...s!==void 0&&{body:JSON.stringify(s)}});if(!r.ok){let u=`Voice request failed: ${r.status} ${r.statusText}`,c="voice_error";try{let d=await r.json();d.error&&(u=d.error),d.code&&(c=d.code)}catch{}throw new p(u,c,r.status)}return await r.json()}function R(){return{stateChange:new Set,transcript:new Set,toolCall:new Set,error:new Set}}function _(i){switch(i){case"listening":case"idle":return"listening";case"thinking":return"thinking";case"speaking":return"speaking";default:return"connecting"}}function L(){return{async startSession(i){i?.signal?.throwIfAborted();let l=await w("/sessions","POST"),s=await import("livekit-client");i?.signal?.throwIfAborted();let o=new s.Room,a=R(),r="connecting",u=!1,c=null,d=e=>{if(!(e===r||r==="ended")){r=e;for(let t of a.stateChange)t(e)}},E=e=>{for(let t of a.error)t(e)},T=()=>{c&&(c.remove(),c=null),d("ended")},V=e=>o.remoteParticipants.get(e)?.kind===s.ParticipantKind.AGENT;o.on(s.RoomEvent.TrackSubscribed,(e,t,n)=>{e.kind===s.Track.Kind.Audio&&n.kind===s.ParticipantKind.AGENT&&(c=e.attach(),c.style.display="none",document.body.appendChild(c))}),o.on(s.RoomEvent.ParticipantAttributesChanged,(e,t)=>{if(t.kind===s.ParticipantKind.AGENT){let n=t.attributes[I];n&&d(_(n))}}),o.on(s.RoomEvent.Disconnected,()=>{T()}),o.registerTextStreamHandler(v,(e,t)=>{let n=t.identity===o.localParticipant.identity?"user":"agent",S=e.info.attributes??{},g=S[x]??e.info.id;(async()=>{try{if(n==="user"){let f=await e.readAll(),m=S[A]==="true";for(let h of a.transcript)h({role:n,segmentId:g,text:f,final:m})}else{let f="";for await(let m of e){f+=m;for(let h of a.transcript)h({role:n,segmentId:g,text:f,final:!1})}for(let m of a.transcript)m({role:n,segmentId:g,text:f,final:!0})}}catch{}})()}),o.registerTextStreamHandler(C,(e,t)=>{V(t.identity)&&(async()=>{try{let n=JSON.parse(await e.readAll());if(!n.method)return;let S=n.status==="done"||n.status==="failed"?n.status:"running";for(let g of a.toolCall)g({method:n.method,status:S,at:n.at??Date.now(),...n.result!==void 0?{result:n.result}:{},...n.resultTruncated===!0?{resultTruncated:!0}:{}})}catch{}})()});let b=()=>{o.disconnect()};i?.signal?.addEventListener("abort",b,{once:!0});try{await o.connect(l.url,l.token),i?.signal?.throwIfAborted();try{await o.localParticipant.setMicrophoneEnabled(!0)}catch(e){let t=e?.name;throw t==="NotAllowedError"||t==="PermissionDeniedError"?new p("Microphone access was denied. Allow microphone access for this site and try again.","microphone_denied"):t==="NotFoundError"?new p("No microphone was found on this device.","microphone_not_found"):e}}catch(e){throw await o.disconnect().catch(()=>{}),T(),i?.signal?.aborted?new DOMException("Voice session aborted","AbortError"):e}finally{i?.signal?.removeEventListener("abort",b)}return{sessionId:l.sessionId,room:l.room,get state(){return r},get isMuted(){return u},on(e,t){let n=a[e];return n.add(t),()=>{n.delete(t)}},async mute(){await o.localParticipant.setMicrophoneEnabled(!1),u=!0},async unmute(){await o.localParticipant.setMicrophoneEnabled(!0),u=!1},async sendText(e){try{await o.localParticipant.sendText(e,{topic:P})}catch(t){throw E(t),t}},async end(){await o.disconnect(),T()}}},async listSessions(i){let l=i?`?cursor=${encodeURIComponent(i)}`:"";return w(`/sessions${l}`,"GET")},async getSession(i){return w(`/sessions/${encodeURIComponent(i)}`,"GET")}}}export{L as createVoiceClient};
1
+ import{a as _,c as V}from"./chunk-5L3RRB4F.js";var A="/_/voice",x="lk.transcription",O="lk.chat",R="ms.voice.tool",L="ms.voice.client-tool",N="ms.voice.client-tool-result",M=32e3,H="lk.transcription_final",$="lk.segment_id",D="lk.agent.state",K=200,G=100;async function E(r,p,a){let o={Authorization:`Bearer ${V().token}`};a!==void 0&&(o["Content-Type"]="application/json");let i=await fetch(`${A}${r}`,{method:p,headers:o,...a!==void 0&&{body:JSON.stringify(a)}});if(!i.ok){let g=`Voice request failed: ${i.status} ${i.statusText}`,d="voice_error";try{let u=await i.json();u.error&&(g=u.error),u.code&&(d=u.code)}catch{}throw new _(g,d,i.status)}return await i.json()}function q(){return{stateChange:new Set,transcript:new Set,toolCall:new Set,error:new Set}}function J(r){switch(r){case"listening":case"idle":return"listening";case"thinking":return"thinking";case"speaking":return"speaking";default:return"connecting"}}function j(){return{async startSession(r){r?.signal?.throwIfAborted();let p=V().token,a=await E("/sessions","POST"),l=await import("livekit-client");r?.signal?.throwIfAborted();let o=new l.Room,i=q(),g=new Map,d="connecting",u=!1,m=null,T=null,v=e=>{if(!(e===d||d==="ended")){d=e;for(let t of i.stateChange)t(e)}},b=e=>{for(let t of i.error)t(e)},y=()=>{m&&(m.remove(),m=null),T&&window.__MS_VOICE__===T&&delete window.__MS_VOICE__,T=null,v("ended")},P=e=>o.remoteParticipants.get(e)?.kind===l.ParticipantKind.AGENT;o.on(l.RoomEvent.TrackSubscribed,(e,t,n)=>{e.kind===l.Track.Kind.Audio&&n.kind===l.ParticipantKind.AGENT&&(m=e.attach(),m.style.display="none",document.body.appendChild(m))}),o.on(l.RoomEvent.ParticipantAttributesChanged,(e,t)=>{if(t.kind===l.ParticipantKind.AGENT){let n=t.attributes[D];n&&v(J(n))}}),o.on(l.RoomEvent.Disconnected,()=>{y()}),o.registerTextStreamHandler(x,(e,t)=>{let n=t.identity===o.localParticipant.identity?"user":"agent",f=e.info.attributes??{},s=f[$]??e.info.id;(async()=>{try{if(n==="user"){let c=await e.readAll(),w=f[H]==="true";for(let k of i.transcript)k({role:n,segmentId:s,text:c,final:w})}else{let c="";for await(let w of e){c+=w;for(let k of i.transcript)k({role:n,segmentId:s,text:c,final:!1})}for(let w of i.transcript)w({role:n,segmentId:s,text:c,final:!0})}}catch{}})()}),o.registerTextStreamHandler(L,e=>{(async()=>{let t;try{t=JSON.parse(await e.readAll())}catch{return}if(!t.id||!t.name)return;let n=async s=>{try{await o.localParticipant.sendText(JSON.stringify({id:t.id,...s}),{topic:N})}catch(c){b(c)}},f=g.get(t.name);if(!f){console.warn(`[voice] client tool "${t.name}" invoked but no handler is registered \u2014 call session.registerClientTool()`),await n({error:"unhandled_client_tool"});return}try{let s=await f(t.args),c=s===void 0?void 0:JSON.stringify(s);if(c!==void 0&&c.length>M){await n({error:"result_too_large"});return}await n(s===void 0?{}:{result:s})}catch(s){await n({error:s instanceof Error?s.message:String(s)})}})()}),o.registerTextStreamHandler(R,(e,t)=>{P(t.identity)&&(async()=>{try{let n=JSON.parse(await e.readAll());if(!n.method)return;let f=n.status==="done"||n.status==="failed"?n.status:"running";for(let s of i.toolCall)s({method:n.method,status:f,at:n.at??Date.now(),...n.result!==void 0?{result:n.result}:{},...n.resultTruncated===!0?{resultTruncated:!0}:{}})}catch{}})()});let I=()=>{o.disconnect()};r?.signal?.addEventListener("abort",I,{once:!0});try{await o.connect(a.url,a.token),r?.signal?.throwIfAborted();try{await o.localParticipant.setMicrophoneEnabled(!0)}catch(e){let t=e?.name;throw t==="NotAllowedError"||t==="PermissionDeniedError"?new _("Microphone access was denied. Allow microphone access for this site and try again.","microphone_denied"):t==="NotFoundError"?new _("No microphone was found on this device.","microphone_not_found"):e}}catch(e){throw await o.disconnect().catch(()=>{}),y(),r?.signal?.aborted?new DOMException("Voice session aborted","AbortError"):e}finally{r?.signal?.removeEventListener("abort",I)}let C={sessionId:a.sessionId,room:a.room,get state(){return d},get isMuted(){return u},on(e,t){let n=i[e];return n.add(t),()=>{n.delete(t)}},async mute(){await o.localParticipant.setMicrophoneEnabled(!1),u=!0},async unmute(){await o.localParticipant.setMicrophoneEnabled(!0),u=!1},async sendText(e){try{await o.localParticipant.sendText(e,{topic:O})}catch(t){throw b(t),t}},registerClientTool(e,t){return g.set(e,t),()=>{g.get(e)===t&&g.delete(e)}},async refreshIdentity(){await E(`/sessions/${encodeURIComponent(a.sessionId)}/refresh-identity`,"POST",{previousToken:p})},async end(){await o.disconnect(),y()}},S=[];i.transcript.add(e=>{let t=S.findIndex(n=>n.segmentId===e.segmentId&&n.role===e.role);t>=0?S[t]=e:(S.push(e),S.length>K&&S.shift())});let h=[];return i.toolCall.add(e=>{h.push(e),h.length>G&&h.shift()}),T={sessionId:a.sessionId,get state(){return d},transcript:S,toolCalls:h,sendText:e=>C.sendText(e),end:()=>C.end()},window.__MS_VOICE__=T,C},async listSessions(r){let p=r?`?cursor=${encodeURIComponent(r)}`:"";return E(`/sessions${p}`,"GET")},async getSession(r){return E(`/sessions/${encodeURIComponent(r)}`,"GET")}}}export{j as createVoiceClient};
2
2
  //# sourceMappingURL=voice.js.map
package/dist/voice.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/voice.ts"],"sourcesContent":["/**\n * Voice client for apps with a voice interface.\n *\n * Imported from the `./voice` subpath so apps that never use voice ship none\n * of it — and `livekit-client` (the one dependency, for the realtime media\n * transport) is loaded dynamically on the first `startSession()` call:\n *\n * ```\n * import { createVoiceClient } from '@mindstudio-ai/interface/voice';\n * ```\n *\n * Flow: `startSession()` mints a session from the platform\n * (`POST /_/voice/sessions`), connects to the media room, publishes the\n * microphone, and returns a {@link VoiceSession} that surfaces the\n * conversation as events — agent state, live captions for both sides, tool\n * activity — plus mute / text-injection / end controls. Agent audio playback\n * is handled internally (a hidden autoplaying element); the app never touches\n * audio elements.\n */\n\nimport { getConfig } from './config.js';\nimport { MindStudioInterfaceError } from './errors.js';\nimport type { Room, RemoteTrack, TextStreamReader } from 'livekit-client';\n\n// ---------------------------------------------------------------------------\n// Wire constants (the voice worker's room contract)\n// ---------------------------------------------------------------------------\n\nconst VOICE_BASE = '/_/voice';\n\n/** Live captions, published by the agent framework. */\nconst TOPIC_TRANSCRIPTION = 'lk.transcription';\n/** Text input into the live conversation (interrupts + replies). */\nconst TOPIC_CHAT = 'lk.chat';\n/** Tool activity, published by the MindStudio voice worker. */\nconst TOPIC_TOOL = 'ms.voice.tool';\n\nconst ATTR_TRANSCRIPTION_FINAL = 'lk.transcription_final';\nconst ATTR_SEGMENT_ID = 'lk.segment_id';\nconst ATTR_AGENT_STATE = 'lk.agent.state';\n\n// ---------------------------------------------------------------------------\n// Session types\n// ---------------------------------------------------------------------------\n\n/** Lifecycle of a live voice session, driven by the agent's published state. */\nexport type VoiceSessionState =\n | 'connecting'\n | 'listening'\n | 'thinking'\n | 'speaking'\n | 'ended';\n\n/** One live-caption update. */\nexport interface VoiceTranscriptEvent {\n /** Whose speech this is. */\n role: 'user' | 'agent';\n /** Stable id for one utterance — updates with the same id replace, not append. */\n segmentId: string;\n /** The utterance text so far (always the full text, never a delta). */\n text: string;\n /** True once this segment won't change again. */\n final: boolean;\n}\n\n/** Tool activity from the agent, for inline status UI. */\nexport interface VoiceToolCallEvent {\n /** The method id being executed. */\n method: string;\n status: 'running' | 'done' | 'failed';\n /** Milliseconds since epoch. */\n at: number;\n /**\n * The tool's return value — present on 'done' only for tools whose voice.md\n * block declares `forwardResult: true`, so the UI can render what the agent\n * just did (a citation, a record, a confirmation) in lockstep with speech.\n * Delivered only to this session's room; platform-guaranteed, no polling.\n */\n result?: unknown;\n /** Set instead of `result` when the payload exceeded the forwarding size\n * cap (~32KB serialized) — fetch the data yourself in that case. */\n resultTruncated?: boolean;\n}\n\nexport interface VoiceSessionCallbacks {\n stateChange: (state: VoiceSessionState) => void;\n transcript: (event: VoiceTranscriptEvent) => void;\n toolCall: (event: VoiceToolCallEvent) => void;\n error: (error: Error) => void;\n}\n\nexport interface StartSessionOptions {\n /** Abort the connection attempt (the promise rejects with `AbortError`). */\n signal?: AbortSignal;\n}\n\n/** A live voice conversation. */\nexport interface VoiceSession {\n /** The platform session id (matches the call record in session history). */\n readonly sessionId: string;\n /** The media room name. */\n readonly room: string;\n /** Current session state. */\n readonly state: VoiceSessionState;\n /** Whether the microphone is currently muted. */\n readonly isMuted: boolean;\n\n /** Subscribe to a session event. Returns an unsubscribe function. */\n on<K extends keyof VoiceSessionCallbacks>(\n event: K,\n callback: VoiceSessionCallbacks[K],\n ): () => void;\n\n /** Mute the microphone (the session stays live). */\n mute(): Promise<void>;\n /** Unmute the microphone. */\n unmute(): Promise<void>;\n /**\n * Inject text into the live conversation — the agent treats it like user\n * speech (interrupts what it's saying and responds). Use for exact strings\n * that are painful aloud: addresses, codes, emails.\n */\n sendText(text: string): Promise<void>;\n /** End the session and release the microphone. */\n end(): Promise<void>;\n}\n\n// ---------------------------------------------------------------------------\n// Call-record types (session history)\n// ---------------------------------------------------------------------------\n\n/** A past voice session (call record), transcript excluded. */\nexport interface VoiceSessionSummary {\n id: string;\n status: 'active' | 'ended' | 'failed';\n startedAt: string;\n endedAt: string | null;\n durationSecs: number | null;\n endedReason: string | null;\n}\n\n/** One persisted transcript entry on a call record. */\nexport interface VoiceTranscriptEntry {\n role: 'user' | 'assistant';\n text: string;\n interrupted?: boolean;\n /** Milliseconds since epoch. */\n at: number;\n}\n\n/** A past voice session with its transcript. */\nexport interface VoiceSessionDetail extends VoiceSessionSummary {\n transcript: VoiceTranscriptEntry[];\n}\n\nexport interface VoiceSessionListPage {\n sessions: VoiceSessionSummary[];\n nextCursor: string | null;\n}\n\n// ---------------------------------------------------------------------------\n// Client interface\n// ---------------------------------------------------------------------------\n\nexport interface VoiceClient {\n /**\n * Start a voice session: mints it from the platform, connects to the media\n * room, and publishes the microphone.\n *\n * @throws {MindStudioInterfaceError} `microphone_denied` when the user\n * refuses microphone access, `voice_concurrency_limit` /\n * `voice_visitor_limit` when the app's session limits are reached,\n * `auth_required` (401) / `role_required` (403) when the interface's\n * auth block denies the caller (route to the app's login flow),\n * `no_voice_config` when the app has no voice interface.\n */\n startSession(options?: StartSessionOptions): Promise<VoiceSession>;\n\n /** List the current user's (or visitor's) past sessions, newest first. */\n listSessions(cursor?: string): Promise<VoiceSessionListPage>;\n\n /** Fetch one past session, transcript included. */\n getSession(sessionId: string): Promise<VoiceSessionDetail>;\n}\n\n// ---------------------------------------------------------------------------\n// Private helpers\n// ---------------------------------------------------------------------------\n\nasync function request<T>(\n path: string,\n method: string,\n body?: unknown,\n): Promise<T> {\n const config = getConfig();\n const headers: Record<string, string> = {\n Authorization: `Bearer ${config.token}`,\n };\n\n if (body !== undefined) {\n headers['Content-Type'] = 'application/json';\n }\n\n const res = await fetch(`${VOICE_BASE}${path}`, {\n method,\n headers,\n ...(body !== undefined && { body: JSON.stringify(body) }),\n });\n\n if (!res.ok) {\n let errorMessage = `Voice request failed: ${res.status} ${res.statusText}`;\n let errorCode = 'voice_error';\n try {\n const err = (await res.json()) as { error?: string; code?: string };\n if (err.error) {\n errorMessage = err.error;\n }\n if (err.code) {\n errorCode = err.code;\n }\n } catch {\n // Response wasn't JSON — use the default message\n }\n throw new MindStudioInterfaceError(errorMessage, errorCode, res.status);\n }\n\n return (await res.json()) as T;\n}\n\ntype Listeners = {\n [K in keyof VoiceSessionCallbacks]: Set<VoiceSessionCallbacks[K]>;\n};\n\nfunction createListeners(): Listeners {\n return {\n stateChange: new Set(),\n transcript: new Set(),\n toolCall: new Set(),\n error: new Set(),\n };\n}\n\n/** Map the agent's published state onto the session's client-facing states. */\nfunction mapAgentState(value: string): VoiceSessionState {\n switch (value) {\n case 'listening':\n case 'idle':\n return 'listening';\n case 'thinking':\n return 'thinking';\n case 'speaking':\n return 'speaking';\n default:\n return 'connecting';\n }\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Create a voice client.\n *\n * @returns A {@link VoiceClient} for starting live sessions and reading past\n * call records.\n *\n * @example\n * ```ts\n * import { createVoiceClient } from '@mindstudio-ai/interface/voice';\n *\n * const voice = createVoiceClient();\n * const session = await voice.startSession();\n *\n * session.on('stateChange', (state) => setOrbState(state));\n * session.on('transcript', ({ role, segmentId, text, final }) =>\n * upsertCaption(segmentId, role, text, final),\n * );\n * session.on('toolCall', ({ method, status }) => showToolStatus(method, status));\n *\n * // ... later\n * await session.end();\n * ```\n */\nexport function createVoiceClient(): VoiceClient {\n return {\n async startSession(options?: StartSessionOptions): Promise<VoiceSession> {\n options?.signal?.throwIfAborted();\n\n const minted = await request<{\n sessionId: string;\n url: string;\n token: string;\n room: string;\n }>('/sessions', 'POST');\n\n // The media transport loads on first use — apps that never start a\n // voice session never fetch it.\n const lk = await import('livekit-client');\n\n options?.signal?.throwIfAborted();\n\n const room: Room = new lk.Room();\n const listeners = createListeners();\n let state: VoiceSessionState = 'connecting';\n let muted = false;\n let audioEl: HTMLMediaElement | null = null;\n\n const setState = (next: VoiceSessionState) => {\n if (next === state || state === 'ended') {\n return;\n }\n state = next;\n for (const cb of listeners.stateChange) {\n cb(next);\n }\n };\n\n const emitError = (error: Error) => {\n for (const cb of listeners.error) {\n cb(error);\n }\n };\n\n const cleanup = () => {\n if (audioEl) {\n audioEl.remove();\n audioEl = null;\n }\n setState('ended');\n };\n\n const isAgent = (identity: string) => {\n const participant = room.remoteParticipants.get(identity);\n return participant?.kind === lk.ParticipantKind.AGENT;\n };\n\n // Agent audio: attach to an SDK-managed hidden element. The agent\n // waits for this subscription before speaking.\n room.on(\n lk.RoomEvent.TrackSubscribed,\n (track: RemoteTrack, _pub, participant) => {\n if (\n track.kind === lk.Track.Kind.Audio &&\n participant.kind === lk.ParticipantKind.AGENT\n ) {\n audioEl = track.attach();\n audioEl.style.display = 'none';\n document.body.appendChild(audioEl);\n }\n },\n );\n\n // Session state from the agent participant's published attribute.\n room.on(\n lk.RoomEvent.ParticipantAttributesChanged,\n (_changed, participant) => {\n if (participant.kind === lk.ParticipantKind.AGENT) {\n const value = participant.attributes[ATTR_AGENT_STATE];\n if (value) {\n setState(mapAgentState(value));\n }\n }\n },\n );\n\n room.on(lk.RoomEvent.Disconnected, () => {\n cleanup();\n });\n\n // Live captions. User transcripts arrive attributed as the local\n // participant (full-replacement streams per segment, final flagged in\n // the attributes); agent transcripts are one delta stream per segment,\n // final when the stream ends.\n room.registerTextStreamHandler(\n TOPIC_TRANSCRIPTION,\n (reader: TextStreamReader, participantInfo) => {\n const role =\n participantInfo.identity === room.localParticipant.identity\n ? 'user'\n : 'agent';\n const attributes = reader.info.attributes ?? {};\n const segmentId = attributes[ATTR_SEGMENT_ID] ?? reader.info.id;\n\n void (async () => {\n try {\n if (role === 'user') {\n const text = await reader.readAll();\n const final = attributes[ATTR_TRANSCRIPTION_FINAL] === 'true';\n for (const cb of listeners.transcript) {\n cb({ role, segmentId, text, final });\n }\n } else {\n let text = '';\n for await (const chunk of reader) {\n text += chunk;\n for (const cb of listeners.transcript) {\n cb({ role, segmentId, text, final: false });\n }\n }\n for (const cb of listeners.transcript) {\n cb({ role, segmentId, text, final: true });\n }\n }\n } catch {\n // A transcript stream dropping mid-read is cosmetic — skip it.\n }\n })();\n },\n );\n\n // Tool activity from the voice worker.\n room.registerTextStreamHandler(\n TOPIC_TOOL,\n (reader: TextStreamReader, participantInfo) => {\n if (!isAgent(participantInfo.identity)) {\n return;\n }\n void (async () => {\n try {\n const parsed = JSON.parse(await reader.readAll()) as {\n method?: string;\n status?: string;\n at?: number;\n result?: unknown;\n resultTruncated?: boolean;\n };\n if (!parsed.method) {\n return;\n }\n const status =\n parsed.status === 'done' || parsed.status === 'failed'\n ? parsed.status\n : 'running';\n for (const cb of listeners.toolCall) {\n cb({\n method: parsed.method,\n status,\n at: parsed.at ?? Date.now(),\n ...(parsed.result !== undefined\n ? { result: parsed.result }\n : {}),\n ...(parsed.resultTruncated === true\n ? { resultTruncated: true }\n : {}),\n });\n }\n } catch {\n // Malformed tool event — skip it.\n }\n })();\n },\n );\n\n const abortHandler = () => {\n void room.disconnect();\n };\n options?.signal?.addEventListener('abort', abortHandler, { once: true });\n\n try {\n await room.connect(minted.url, minted.token);\n options?.signal?.throwIfAborted();\n\n try {\n await room.localParticipant.setMicrophoneEnabled(true);\n } catch (err) {\n const name = (err as Error)?.name;\n if (name === 'NotAllowedError' || name === 'PermissionDeniedError') {\n throw new MindStudioInterfaceError(\n 'Microphone access was denied. Allow microphone access for this site and try again.',\n 'microphone_denied',\n );\n }\n if (name === 'NotFoundError') {\n throw new MindStudioInterfaceError(\n 'No microphone was found on this device.',\n 'microphone_not_found',\n );\n }\n throw err;\n }\n } catch (err) {\n await room.disconnect().catch(() => undefined);\n cleanup();\n if (options?.signal?.aborted) {\n throw new DOMException('Voice session aborted', 'AbortError');\n }\n throw err;\n } finally {\n options?.signal?.removeEventListener('abort', abortHandler);\n }\n\n return {\n sessionId: minted.sessionId,\n room: minted.room,\n get state() {\n return state;\n },\n get isMuted() {\n return muted;\n },\n on(event, callback) {\n const set = listeners[event] as Set<typeof callback>;\n set.add(callback);\n return () => {\n set.delete(callback);\n };\n },\n async mute() {\n await room.localParticipant.setMicrophoneEnabled(false);\n muted = true;\n },\n async unmute() {\n await room.localParticipant.setMicrophoneEnabled(true);\n muted = false;\n },\n async sendText(text: string) {\n try {\n await room.localParticipant.sendText(text, { topic: TOPIC_CHAT });\n } catch (err) {\n emitError(err as Error);\n throw err;\n }\n },\n async end() {\n await room.disconnect();\n cleanup();\n },\n };\n },\n\n async listSessions(cursor?: string): Promise<VoiceSessionListPage> {\n const query = cursor ? `?cursor=${encodeURIComponent(cursor)}` : '';\n return request<VoiceSessionListPage>(`/sessions${query}`, 'GET');\n },\n\n async getSession(sessionId: string): Promise<VoiceSessionDetail> {\n return request<VoiceSessionDetail>(\n `/sessions/${encodeURIComponent(sessionId)}`,\n 'GET',\n );\n },\n };\n}\n"],"mappings":"+CA4BA,IAAMA,EAAa,WAGbC,EAAsB,mBAEtBC,EAAa,UAEbC,EAAa,gBAEbC,EAA2B,yBAC3BC,EAAkB,gBAClBC,EAAmB,iBAsJzB,eAAeC,EACbC,EACAC,EACAC,EACY,CAEZ,IAAMC,EAAkC,CACtC,cAAe,UAFFC,EAAU,EAES,KAAK,EACvC,EAEIF,IAAS,SACXC,EAAQ,cAAc,EAAI,oBAG5B,IAAME,EAAM,MAAM,MAAM,GAAGb,CAAU,GAAGQ,CAAI,GAAI,CAC9C,OAAAC,EACA,QAAAE,EACA,GAAID,IAAS,QAAa,CAAE,KAAM,KAAK,UAAUA,CAAI,CAAE,CACzD,CAAC,EAED,GAAI,CAACG,EAAI,GAAI,CACX,IAAIC,EAAe,yBAAyBD,EAAI,MAAM,IAAIA,EAAI,UAAU,GACpEE,EAAY,cAChB,GAAI,CACF,IAAMC,EAAO,MAAMH,EAAI,KAAK,EACxBG,EAAI,QACNF,EAAeE,EAAI,OAEjBA,EAAI,OACND,EAAYC,EAAI,KAEpB,MAAQ,CAER,CACA,MAAM,IAAIC,EAAyBH,EAAcC,EAAWF,EAAI,MAAM,CACxE,CAEA,OAAQ,MAAMA,EAAI,KAAK,CACzB,CAMA,SAASK,GAA6B,CACpC,MAAO,CACL,YAAa,IAAI,IACjB,WAAY,IAAI,IAChB,SAAU,IAAI,IACd,MAAO,IAAI,GACb,CACF,CAGA,SAASC,EAAcC,EAAkC,CACvD,OAAQA,EAAO,CACb,IAAK,YACL,IAAK,OACH,MAAO,YACT,IAAK,WACH,MAAO,WACT,IAAK,WACH,MAAO,WACT,QACE,MAAO,YACX,CACF,CA6BO,SAASC,GAAiC,CAC/C,MAAO,CACL,MAAM,aAAaC,EAAsD,CACvEA,GAAS,QAAQ,eAAe,EAEhC,IAAMC,EAAS,MAAMhB,EAKlB,YAAa,MAAM,EAIhBiB,EAAK,KAAM,QAAO,gBAAgB,EAExCF,GAAS,QAAQ,eAAe,EAEhC,IAAMG,EAAa,IAAID,EAAG,KACpBE,EAAYR,EAAgB,EAC9BS,EAA2B,aAC3BC,EAAQ,GACRC,EAAmC,KAEjCC,EAAYC,GAA4B,CAC5C,GAAI,EAAAA,IAASJ,GAASA,IAAU,SAGhC,CAAAA,EAAQI,EACR,QAAWC,KAAMN,EAAU,YACzBM,EAAGD,CAAI,EAEX,EAEME,EAAaC,GAAiB,CAClC,QAAWF,KAAMN,EAAU,MACzBM,EAAGE,CAAK,CAEZ,EAEMC,EAAU,IAAM,CAChBN,IACFA,EAAQ,OAAO,EACfA,EAAU,MAEZC,EAAS,OAAO,CAClB,EAEMM,EAAWC,GACKZ,EAAK,mBAAmB,IAAIY,CAAQ,GACpC,OAASb,EAAG,gBAAgB,MAKlDC,EAAK,GACHD,EAAG,UAAU,gBACb,CAACc,EAAoBC,EAAMC,IAAgB,CAEvCF,EAAM,OAASd,EAAG,MAAM,KAAK,OAC7BgB,EAAY,OAAShB,EAAG,gBAAgB,QAExCK,EAAUS,EAAM,OAAO,EACvBT,EAAQ,MAAM,QAAU,OACxB,SAAS,KAAK,YAAYA,CAAO,EAErC,CACF,EAGAJ,EAAK,GACHD,EAAG,UAAU,6BACb,CAACiB,EAAUD,IAAgB,CACzB,GAAIA,EAAY,OAAShB,EAAG,gBAAgB,MAAO,CACjD,IAAMJ,EAAQoB,EAAY,WAAWlC,CAAgB,EACjDc,GACFU,EAASX,EAAcC,CAAK,CAAC,CAEjC,CACF,CACF,EAEAK,EAAK,GAAGD,EAAG,UAAU,aAAc,IAAM,CACvCW,EAAQ,CACV,CAAC,EAMDV,EAAK,0BACHxB,EACA,CAACyC,EAA0BC,IAAoB,CAC7C,IAAMC,EACJD,EAAgB,WAAalB,EAAK,iBAAiB,SAC/C,OACA,QACAoB,EAAaH,EAAO,KAAK,YAAc,CAAC,EACxCI,EAAYD,EAAWxC,CAAe,GAAKqC,EAAO,KAAK,IAEvD,SAAY,CAChB,GAAI,CACF,GAAIE,IAAS,OAAQ,CACnB,IAAMG,EAAO,MAAML,EAAO,QAAQ,EAC5BM,EAAQH,EAAWzC,CAAwB,IAAM,OACvD,QAAW4B,KAAMN,EAAU,WACzBM,EAAG,CAAE,KAAAY,EAAM,UAAAE,EAAW,KAAAC,EAAM,MAAAC,CAAM,CAAC,CAEvC,KAAO,CACL,IAAID,EAAO,GACX,cAAiBE,KAASP,EAAQ,CAChCK,GAAQE,EACR,QAAWjB,KAAMN,EAAU,WACzBM,EAAG,CAAE,KAAAY,EAAM,UAAAE,EAAW,KAAAC,EAAM,MAAO,EAAM,CAAC,CAE9C,CACA,QAAWf,KAAMN,EAAU,WACzBM,EAAG,CAAE,KAAAY,EAAM,UAAAE,EAAW,KAAAC,EAAM,MAAO,EAAK,CAAC,CAE7C,CACF,MAAQ,CAER,CACF,GAAG,CACL,CACF,EAGAtB,EAAK,0BACHtB,EACA,CAACuC,EAA0BC,IAAoB,CACxCP,EAAQO,EAAgB,QAAQ,IAG/B,SAAY,CAChB,GAAI,CACF,IAAMO,EAAS,KAAK,MAAM,MAAMR,EAAO,QAAQ,CAAC,EAOhD,GAAI,CAACQ,EAAO,OACV,OAEF,IAAMC,EACJD,EAAO,SAAW,QAAUA,EAAO,SAAW,SAC1CA,EAAO,OACP,UACN,QAAWlB,KAAMN,EAAU,SACzBM,EAAG,CACD,OAAQkB,EAAO,OACf,OAAAC,EACA,GAAID,EAAO,IAAM,KAAK,IAAI,EAC1B,GAAIA,EAAO,SAAW,OAClB,CAAE,OAAQA,EAAO,MAAO,EACxB,CAAC,EACL,GAAIA,EAAO,kBAAoB,GAC3B,CAAE,gBAAiB,EAAK,EACxB,CAAC,CACP,CAAC,CAEL,MAAQ,CAER,CACF,GAAG,CACL,CACF,EAEA,IAAME,EAAe,IAAM,CACpB3B,EAAK,WAAW,CACvB,EACAH,GAAS,QAAQ,iBAAiB,QAAS8B,EAAc,CAAE,KAAM,EAAK,CAAC,EAEvE,GAAI,CACF,MAAM3B,EAAK,QAAQF,EAAO,IAAKA,EAAO,KAAK,EAC3CD,GAAS,QAAQ,eAAe,EAEhC,GAAI,CACF,MAAMG,EAAK,iBAAiB,qBAAqB,EAAI,CACvD,OAAST,EAAK,CACZ,IAAMqC,EAAQrC,GAAe,KAC7B,MAAIqC,IAAS,mBAAqBA,IAAS,wBACnC,IAAIpC,EACR,qFACA,mBACF,EAEEoC,IAAS,gBACL,IAAIpC,EACR,0CACA,sBACF,EAEID,CACR,CACF,OAASA,EAAK,CAGZ,MAFA,MAAMS,EAAK,WAAW,EAAE,MAAM,IAAG,EAAY,EAC7CU,EAAQ,EACJb,GAAS,QAAQ,QACb,IAAI,aAAa,wBAAyB,YAAY,EAExDN,CACR,QAAE,CACAM,GAAS,QAAQ,oBAAoB,QAAS8B,CAAY,CAC5D,CAEA,MAAO,CACL,UAAW7B,EAAO,UAClB,KAAMA,EAAO,KACb,IAAI,OAAQ,CACV,OAAOI,CACT,EACA,IAAI,SAAU,CACZ,OAAOC,CACT,EACA,GAAG0B,EAAOC,EAAU,CAClB,IAAMC,EAAM9B,EAAU4B,CAAK,EAC3B,OAAAE,EAAI,IAAID,CAAQ,EACT,IAAM,CACXC,EAAI,OAAOD,CAAQ,CACrB,CACF,EACA,MAAM,MAAO,CACX,MAAM9B,EAAK,iBAAiB,qBAAqB,EAAK,EACtDG,EAAQ,EACV,EACA,MAAM,QAAS,CACb,MAAMH,EAAK,iBAAiB,qBAAqB,EAAI,EACrDG,EAAQ,EACV,EACA,MAAM,SAASmB,EAAc,CAC3B,GAAI,CACF,MAAMtB,EAAK,iBAAiB,SAASsB,EAAM,CAAE,MAAO7C,CAAW,CAAC,CAClE,OAASc,EAAK,CACZ,MAAAiB,EAAUjB,CAAY,EAChBA,CACR,CACF,EACA,MAAM,KAAM,CACV,MAAMS,EAAK,WAAW,EACtBU,EAAQ,CACV,CACF,CACF,EAEA,MAAM,aAAasB,EAAgD,CACjE,IAAMC,EAAQD,EAAS,WAAW,mBAAmBA,CAAM,CAAC,GAAK,GACjE,OAAOlD,EAA8B,YAAYmD,CAAK,GAAI,KAAK,CACjE,EAEA,MAAM,WAAWC,EAAgD,CAC/D,OAAOpD,EACL,aAAa,mBAAmBoD,CAAS,CAAC,GAC1C,KACF,CACF,CACF,CACF","names":["VOICE_BASE","TOPIC_TRANSCRIPTION","TOPIC_CHAT","TOPIC_TOOL","ATTR_TRANSCRIPTION_FINAL","ATTR_SEGMENT_ID","ATTR_AGENT_STATE","request","path","method","body","headers","getConfig","res","errorMessage","errorCode","err","MindStudioInterfaceError","createListeners","mapAgentState","value","createVoiceClient","options","minted","lk","room","listeners","state","muted","audioEl","setState","next","cb","emitError","error","cleanup","isAgent","identity","track","_pub","participant","_changed","reader","participantInfo","role","attributes","segmentId","text","final","chunk","parsed","status","abortHandler","name","event","callback","set","cursor","query","sessionId"]}
1
+ {"version":3,"sources":["../src/voice.ts"],"sourcesContent":["/**\n * Voice client for apps with a voice interface.\n *\n * Imported from the `./voice` subpath so apps that never use voice ship none\n * of it — and `livekit-client` (the one dependency, for the realtime media\n * transport) is loaded dynamically on the first `startSession()` call:\n *\n * ```\n * import { createVoiceClient } from '@mindstudio-ai/interface/voice';\n * ```\n *\n * Flow: `startSession()` mints a session from the platform\n * (`POST /_/voice/sessions`), connects to the media room, publishes the\n * microphone, and returns a {@link VoiceSession} that surfaces the\n * conversation as events — agent state, live captions for both sides, tool\n * activity — plus mute / text-injection / end controls. Agent audio playback\n * is handled internally (a hidden autoplaying element); the app never touches\n * audio elements.\n */\n\nimport { getConfig } from './config.js';\nimport { MindStudioInterfaceError } from './errors.js';\nimport type { Room, RemoteTrack, TextStreamReader } from 'livekit-client';\n\n// ---------------------------------------------------------------------------\n// Wire constants (the voice worker's room contract)\n// ---------------------------------------------------------------------------\n\nconst VOICE_BASE = '/_/voice';\n\n/** Live captions, published by the agent framework. */\nconst TOPIC_TRANSCRIPTION = 'lk.transcription';\n/** Text input into the live conversation (interrupts + replies). */\nconst TOPIC_CHAT = 'lk.chat';\n/** Tool activity, published by the MindStudio voice worker. */\nconst TOPIC_TOOL = 'ms.voice.tool';\nconst TOPIC_CLIENT_TOOL = 'ms.voice.client-tool';\nconst TOPIC_CLIENT_TOOL_RESULT = 'ms.voice.client-tool-result';\n// Client-tool handler returns must fit the same forwarding cap as tool\n// results; oversize returns become an error the agent can speak around.\nconst CLIENT_TOOL_RESULT_MAX_CHARS = 32_000;\n\nconst ATTR_TRANSCRIPTION_FINAL = 'lk.transcription_final';\nconst ATTR_SEGMENT_ID = 'lk.segment_id';\nconst ATTR_AGENT_STATE = 'lk.agent.state';\n\n// ---------------------------------------------------------------------------\n// Session types\n// ---------------------------------------------------------------------------\n\n/** Lifecycle of a live voice session, driven by the agent's published state. */\nexport type VoiceSessionState =\n | 'connecting'\n | 'listening'\n | 'thinking'\n | 'speaking'\n | 'ended';\n\n/** One live-caption update. */\nexport interface VoiceTranscriptEvent {\n /** Whose speech this is. */\n role: 'user' | 'agent';\n /** Stable id for one utterance — updates with the same id replace, not append. */\n segmentId: string;\n /** The utterance text so far (always the full text, never a delta). */\n text: string;\n /** True once this segment won't change again. */\n final: boolean;\n}\n\n/** Tool activity from the agent, for inline status UI. */\nexport interface VoiceToolCallEvent {\n /** The method id being executed. */\n method: string;\n status: 'running' | 'done' | 'failed';\n /** Milliseconds since epoch. */\n at: number;\n /**\n * The tool's return value — present on every 'done' (unless truncated), so\n * the UI can render what the agent just did (a citation, a record, a\n * confirmation) in lockstep with speech. Delivered only to this session's\n * room — the same security context as the invocation. Failed calls carry\n * no result.\n */\n result?: unknown;\n /** Set instead of `result` when the payload exceeded the forwarding size\n * cap (~32KB serialized) — fetch the data yourself in that case. */\n resultTruncated?: boolean;\n}\n\nexport interface VoiceSessionCallbacks {\n stateChange: (state: VoiceSessionState) => void;\n transcript: (event: VoiceTranscriptEvent) => void;\n toolCall: (event: VoiceToolCallEvent) => void;\n error: (error: Error) => void;\n}\n\nexport interface StartSessionOptions {\n /** Abort the connection attempt (the promise rejects with `AbortError`). */\n signal?: AbortSignal;\n}\n\n/** A live voice conversation. */\nexport interface VoiceSession {\n /** The platform session id (matches the call record in session history). */\n readonly sessionId: string;\n /** The media room name. */\n readonly room: string;\n /** Current session state. */\n readonly state: VoiceSessionState;\n /** Whether the microphone is currently muted. */\n readonly isMuted: boolean;\n\n /** Subscribe to a session event. Returns an unsubscribe function. */\n on<K extends keyof VoiceSessionCallbacks>(\n event: K,\n callback: VoiceSessionCallbacks[K],\n ): () => void;\n\n /** Mute the microphone (the session stays live). */\n mute(): Promise<void>;\n /** Unmute the microphone. */\n unmute(): Promise<void>;\n /**\n * Inject text into the live conversation — the agent treats it like user\n * speech (interrupts what it's saying and responds). Use for exact strings\n * that are painful aloud: addresses, codes, emails.\n */\n sendText(text: string): Promise<void>;\n /**\n * Register a handler for a client tool — a tool declared in voice.md with\n * `target: \"client\"`, whose effect lives in this browser (open a sheet,\n * navigate, highlight). When the agent invokes it, the handler runs and its\n * return value goes back to the agent as the tool result — a real\n * request/response, so the agent knows the action happened. Throwing (or an\n * unregistered tool) returns an error the agent speaks around; the agent\n * waits up to ~30s. One handler per tool name (later registrations\n * replace); returns an unregister function.\n */\n registerClientTool(\n name: string,\n handler: (args: unknown) => unknown | Promise<unknown>,\n ): () => void;\n\n /**\n * Upgrade this live session from anonymous to the now-signed-in user —\n * call it right after your in-app verification succeeds (progressive\n * auth). Subsequent tool calls run as the verified user with their roles,\n * and the agent's Current User context refreshes, WITHOUT tearing down\n * the conversation. Requires the session to have been started anonymously\n * (an already-identified session rejects with `already_identified`);\n * the SDK proves ownership with the token the session was minted under.\n * On failure, ending and restarting the session is the fallback.\n */\n refreshIdentity(): Promise<void>;\n /** End the session and release the microphone. */\n end(): Promise<void>;\n}\n\n/**\n * The active session's QA/debug handle, published on `window.__MS_VOICE__`\n * while a session is live (cleared when it ends; last session wins).\n *\n * Exists so automation that can't produce audio — the platform's browser QA\n * agent driving the app in headless Chrome — can converse with the voice\n * agent by injected text and assert on what it said and which tools ran,\n * while the app's real UI (client-tool cards, state orb, captions) renders\n * normally. It grants nothing the page's own scripts don't already have:\n * it's the same session object, in the same execution context, made\n * reachable instead of trapped in the app's closure.\n */\nexport interface VoiceQaHandle {\n sessionId: string;\n /** Live session state (reads through to the session). */\n readonly state: VoiceSessionState;\n /** Inject a user turn — same as `session.sendText`. */\n sendText(text: string): Promise<void>;\n /**\n * Caption history so far, one entry per segment (entries update in place as\n * a segment grows; `final` marks settled ones). Read before `end()` — the\n * handle is removed when the session ends.\n */\n transcript: VoiceTranscriptEvent[];\n /** Tool activity so far, in arrival order (`done` entries carry `result`). */\n toolCalls: VoiceToolCallEvent[];\n /** End the session — same as `session.end`. */\n end(): Promise<void>;\n}\n\ndeclare global {\n interface Window {\n /** Active voice session's QA/debug handle — see {@link VoiceQaHandle}. */\n __MS_VOICE__?: VoiceQaHandle;\n }\n}\n\n// Buffer bounds for the QA handle. Segments update in place, so the\n// transcript cap is a count of utterances, not events; both caps exist only\n// so a long-running session can't grow the page's memory unboundedly.\nconst QA_TRANSCRIPT_MAX_SEGMENTS = 200;\nconst QA_TOOL_CALLS_MAX = 100;\n\n// ---------------------------------------------------------------------------\n// Call-record types (session history)\n// ---------------------------------------------------------------------------\n\n/** A past voice session (call record), transcript excluded. */\nexport interface VoiceSessionSummary {\n id: string;\n status: 'active' | 'ended' | 'failed';\n startedAt: string;\n endedAt: string | null;\n durationSecs: number | null;\n endedReason: string | null;\n}\n\n/** One persisted transcript entry on a call record. */\nexport interface VoiceTranscriptEntry {\n role: 'user' | 'assistant';\n text: string;\n interrupted?: boolean;\n /** Milliseconds since epoch. */\n at: number;\n}\n\n/** A past voice session with its transcript. */\nexport interface VoiceSessionDetail extends VoiceSessionSummary {\n transcript: VoiceTranscriptEntry[];\n}\n\nexport interface VoiceSessionListPage {\n sessions: VoiceSessionSummary[];\n nextCursor: string | null;\n}\n\n// ---------------------------------------------------------------------------\n// Client interface\n// ---------------------------------------------------------------------------\n\nexport interface VoiceClient {\n /**\n * Start a voice session: mints it from the platform, connects to the media\n * room, and publishes the microphone.\n *\n * @throws {MindStudioInterfaceError} `microphone_denied` when the user\n * refuses microphone access, `voice_concurrency_limit` /\n * `voice_visitor_limit` when the app's session limits are reached,\n * `auth_required` (401) / `role_required` (403) when the interface's\n * auth block denies the caller (route to the app's login flow),\n * `no_voice_config` when the app has no voice interface.\n */\n startSession(options?: StartSessionOptions): Promise<VoiceSession>;\n\n /** List the current user's (or visitor's) past sessions, newest first. */\n listSessions(cursor?: string): Promise<VoiceSessionListPage>;\n\n /** Fetch one past session, transcript included. */\n getSession(sessionId: string): Promise<VoiceSessionDetail>;\n}\n\n// ---------------------------------------------------------------------------\n// Private helpers\n// ---------------------------------------------------------------------------\n\nasync function request<T>(\n path: string,\n method: string,\n body?: unknown,\n): Promise<T> {\n const config = getConfig();\n const headers: Record<string, string> = {\n Authorization: `Bearer ${config.token}`,\n };\n\n if (body !== undefined) {\n headers['Content-Type'] = 'application/json';\n }\n\n const res = await fetch(`${VOICE_BASE}${path}`, {\n method,\n headers,\n ...(body !== undefined && { body: JSON.stringify(body) }),\n });\n\n if (!res.ok) {\n let errorMessage = `Voice request failed: ${res.status} ${res.statusText}`;\n let errorCode = 'voice_error';\n try {\n const err = (await res.json()) as { error?: string; code?: string };\n if (err.error) {\n errorMessage = err.error;\n }\n if (err.code) {\n errorCode = err.code;\n }\n } catch {\n // Response wasn't JSON — use the default message\n }\n throw new MindStudioInterfaceError(errorMessage, errorCode, res.status);\n }\n\n return (await res.json()) as T;\n}\n\ntype Listeners = {\n [K in keyof VoiceSessionCallbacks]: Set<VoiceSessionCallbacks[K]>;\n};\n\nfunction createListeners(): Listeners {\n return {\n stateChange: new Set(),\n transcript: new Set(),\n toolCall: new Set(),\n error: new Set(),\n };\n}\n\n/** Map the agent's published state onto the session's client-facing states. */\nfunction mapAgentState(value: string): VoiceSessionState {\n switch (value) {\n case 'listening':\n case 'idle':\n return 'listening';\n case 'thinking':\n return 'thinking';\n case 'speaking':\n return 'speaking';\n default:\n return 'connecting';\n }\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Create a voice client.\n *\n * @returns A {@link VoiceClient} for starting live sessions and reading past\n * call records.\n *\n * @example\n * ```ts\n * import { createVoiceClient } from '@mindstudio-ai/interface/voice';\n *\n * const voice = createVoiceClient();\n * const session = await voice.startSession();\n *\n * session.on('stateChange', (state) => setOrbState(state));\n * session.on('transcript', ({ role, segmentId, text, final }) =>\n * upsertCaption(segmentId, role, text, final),\n * );\n * session.on('toolCall', ({ method, status }) => showToolStatus(method, status));\n *\n * // ... later\n * await session.end();\n * ```\n */\nexport function createVoiceClient(): VoiceClient {\n return {\n async startSession(options?: StartSessionOptions): Promise<VoiceSession> {\n options?.signal?.throwIfAborted();\n\n // Snapshot the bearer this session is minted under: after an in-app\n // login the app's config token is REPLACED (a fresh interface session),\n // and refreshIdentity() must prove ownership with the original one.\n const mintTimeToken = getConfig().token;\n\n const minted = await request<{\n sessionId: string;\n url: string;\n token: string;\n room: string;\n }>('/sessions', 'POST');\n\n // The media transport loads on first use — apps that never start a\n // voice session never fetch it.\n const lk = await import('livekit-client');\n\n options?.signal?.throwIfAborted();\n\n const room: Room = new lk.Room();\n const listeners = createListeners();\n const clientToolHandlers = new Map<\n string,\n (args: unknown) => unknown | Promise<unknown>\n >();\n let state: VoiceSessionState = 'connecting';\n let muted = false;\n let audioEl: HTMLMediaElement | null = null;\n let qaHandle: VoiceQaHandle | null = null;\n\n const setState = (next: VoiceSessionState) => {\n if (next === state || state === 'ended') {\n return;\n }\n state = next;\n for (const cb of listeners.stateChange) {\n cb(next);\n }\n };\n\n const emitError = (error: Error) => {\n for (const cb of listeners.error) {\n cb(error);\n }\n };\n\n const cleanup = () => {\n if (audioEl) {\n audioEl.remove();\n audioEl = null;\n }\n // Only remove the QA handle if it's still ours — a newer session may\n // have replaced it already (last session wins).\n if (qaHandle && window.__MS_VOICE__ === qaHandle) {\n delete window.__MS_VOICE__;\n }\n qaHandle = null;\n setState('ended');\n };\n\n const isAgent = (identity: string) => {\n const participant = room.remoteParticipants.get(identity);\n return participant?.kind === lk.ParticipantKind.AGENT;\n };\n\n // Agent audio: attach to an SDK-managed hidden element. The agent\n // waits for this subscription before speaking.\n room.on(\n lk.RoomEvent.TrackSubscribed,\n (track: RemoteTrack, _pub, participant) => {\n if (\n track.kind === lk.Track.Kind.Audio &&\n participant.kind === lk.ParticipantKind.AGENT\n ) {\n audioEl = track.attach();\n audioEl.style.display = 'none';\n document.body.appendChild(audioEl);\n }\n },\n );\n\n // Session state from the agent participant's published attribute.\n room.on(\n lk.RoomEvent.ParticipantAttributesChanged,\n (_changed, participant) => {\n if (participant.kind === lk.ParticipantKind.AGENT) {\n const value = participant.attributes[ATTR_AGENT_STATE];\n if (value) {\n setState(mapAgentState(value));\n }\n }\n },\n );\n\n room.on(lk.RoomEvent.Disconnected, () => {\n cleanup();\n });\n\n // Live captions. User transcripts arrive attributed as the local\n // participant (full-replacement streams per segment, final flagged in\n // the attributes); agent transcripts are one delta stream per segment,\n // final when the stream ends.\n room.registerTextStreamHandler(\n TOPIC_TRANSCRIPTION,\n (reader: TextStreamReader, participantInfo) => {\n const role =\n participantInfo.identity === room.localParticipant.identity\n ? 'user'\n : 'agent';\n const attributes = reader.info.attributes ?? {};\n const segmentId = attributes[ATTR_SEGMENT_ID] ?? reader.info.id;\n\n void (async () => {\n try {\n if (role === 'user') {\n const text = await reader.readAll();\n const final = attributes[ATTR_TRANSCRIPTION_FINAL] === 'true';\n for (const cb of listeners.transcript) {\n cb({ role, segmentId, text, final });\n }\n } else {\n let text = '';\n for await (const chunk of reader) {\n text += chunk;\n for (const cb of listeners.transcript) {\n cb({ role, segmentId, text, final: false });\n }\n }\n for (const cb of listeners.transcript) {\n cb({ role, segmentId, text, final: true });\n }\n }\n } catch {\n // A transcript stream dropping mid-read is cosmetic — skip it.\n }\n })();\n },\n );\n\n // Tool activity from the voice worker.\n // Client-tool invocations from the agent: run the app's handler and\n // stream the outcome back. Every path answers (unhandled/thrown/oversize\n // included) so the agent never waits out the timeout unnecessarily.\n room.registerTextStreamHandler(TOPIC_CLIENT_TOOL, (reader) => {\n void (async () => {\n let invocation: { id?: string; name?: string; args?: unknown };\n try {\n invocation = JSON.parse(await reader.readAll());\n } catch {\n return; // Malformed invocation — nothing to correlate a reply to.\n }\n if (!invocation.id || !invocation.name) {\n return;\n }\n const respond = async (response: Record<string, unknown>) => {\n try {\n await room.localParticipant.sendText(\n JSON.stringify({ id: invocation.id, ...response }),\n { topic: TOPIC_CLIENT_TOOL_RESULT },\n );\n } catch (err) {\n emitError(err as Error);\n }\n };\n\n const handler = clientToolHandlers.get(invocation.name);\n if (!handler) {\n console.warn(\n `[voice] client tool \"${invocation.name}\" invoked but no handler is registered — call session.registerClientTool()`,\n );\n await respond({ error: 'unhandled_client_tool' });\n return;\n }\n try {\n const result = await handler(invocation.args);\n const serialized =\n result === undefined ? undefined : JSON.stringify(result);\n if (\n serialized !== undefined &&\n serialized.length > CLIENT_TOOL_RESULT_MAX_CHARS\n ) {\n await respond({ error: 'result_too_large' });\n return;\n }\n await respond(result === undefined ? {} : { result });\n } catch (err) {\n await respond({\n error: err instanceof Error ? err.message : String(err),\n });\n }\n })();\n });\n\n room.registerTextStreamHandler(\n TOPIC_TOOL,\n (reader: TextStreamReader, participantInfo) => {\n if (!isAgent(participantInfo.identity)) {\n return;\n }\n void (async () => {\n try {\n const parsed = JSON.parse(await reader.readAll()) as {\n method?: string;\n status?: string;\n at?: number;\n result?: unknown;\n resultTruncated?: boolean;\n };\n if (!parsed.method) {\n return;\n }\n const status =\n parsed.status === 'done' || parsed.status === 'failed'\n ? parsed.status\n : 'running';\n for (const cb of listeners.toolCall) {\n cb({\n method: parsed.method,\n status,\n at: parsed.at ?? Date.now(),\n ...(parsed.result !== undefined\n ? { result: parsed.result }\n : {}),\n ...(parsed.resultTruncated === true\n ? { resultTruncated: true }\n : {}),\n });\n }\n } catch {\n // Malformed tool event — skip it.\n }\n })();\n },\n );\n\n const abortHandler = () => {\n void room.disconnect();\n };\n options?.signal?.addEventListener('abort', abortHandler, { once: true });\n\n try {\n await room.connect(minted.url, minted.token);\n options?.signal?.throwIfAborted();\n\n try {\n await room.localParticipant.setMicrophoneEnabled(true);\n } catch (err) {\n const name = (err as Error)?.name;\n if (name === 'NotAllowedError' || name === 'PermissionDeniedError') {\n throw new MindStudioInterfaceError(\n 'Microphone access was denied. Allow microphone access for this site and try again.',\n 'microphone_denied',\n );\n }\n if (name === 'NotFoundError') {\n throw new MindStudioInterfaceError(\n 'No microphone was found on this device.',\n 'microphone_not_found',\n );\n }\n throw err;\n }\n } catch (err) {\n await room.disconnect().catch(() => undefined);\n cleanup();\n if (options?.signal?.aborted) {\n throw new DOMException('Voice session aborted', 'AbortError');\n }\n throw err;\n } finally {\n options?.signal?.removeEventListener('abort', abortHandler);\n }\n\n const session: VoiceSession = {\n sessionId: minted.sessionId,\n room: minted.room,\n get state() {\n return state;\n },\n get isMuted() {\n return muted;\n },\n on(event, callback) {\n const set = listeners[event] as Set<typeof callback>;\n set.add(callback);\n return () => {\n set.delete(callback);\n };\n },\n async mute() {\n await room.localParticipant.setMicrophoneEnabled(false);\n muted = true;\n },\n async unmute() {\n await room.localParticipant.setMicrophoneEnabled(true);\n muted = false;\n },\n async sendText(text: string) {\n try {\n await room.localParticipant.sendText(text, { topic: TOPIC_CHAT });\n } catch (err) {\n emitError(err as Error);\n throw err;\n }\n },\n registerClientTool(\n name: string,\n handler: (args: unknown) => unknown | Promise<unknown>,\n ) {\n clientToolHandlers.set(name, handler);\n return () => {\n if (clientToolHandlers.get(name) === handler) {\n clientToolHandlers.delete(name);\n }\n };\n },\n async refreshIdentity() {\n await request(\n `/sessions/${encodeURIComponent(minted.sessionId)}/refresh-identity`,\n 'POST',\n { previousToken: mintTimeToken },\n );\n },\n async end() {\n await room.disconnect();\n cleanup();\n },\n };\n\n // Publish the QA/debug handle (see VoiceQaHandle). Its buffers\n // subscribe through the same listener sets as app callbacks. Transcript\n // entries upsert by segment: events carry the full text so far, and the\n // agent side emits cumulative partials per chunk, so appending would\n // flood the buffer with near-duplicates of one utterance.\n const qaTranscript: VoiceTranscriptEvent[] = [];\n listeners.transcript.add((event) => {\n const idx = qaTranscript.findIndex(\n (e) => e.segmentId === event.segmentId && e.role === event.role,\n );\n if (idx >= 0) {\n qaTranscript[idx] = event;\n } else {\n qaTranscript.push(event);\n if (qaTranscript.length > QA_TRANSCRIPT_MAX_SEGMENTS) {\n qaTranscript.shift();\n }\n }\n });\n const qaToolCalls: VoiceToolCallEvent[] = [];\n listeners.toolCall.add((event) => {\n qaToolCalls.push(event);\n if (qaToolCalls.length > QA_TOOL_CALLS_MAX) {\n qaToolCalls.shift();\n }\n });\n qaHandle = {\n sessionId: minted.sessionId,\n get state() {\n return state;\n },\n transcript: qaTranscript,\n toolCalls: qaToolCalls,\n sendText: (text) => session.sendText(text),\n end: () => session.end(),\n };\n window.__MS_VOICE__ = qaHandle;\n\n return session;\n },\n\n async listSessions(cursor?: string): Promise<VoiceSessionListPage> {\n const query = cursor ? `?cursor=${encodeURIComponent(cursor)}` : '';\n return request<VoiceSessionListPage>(`/sessions${query}`, 'GET');\n },\n\n async getSession(sessionId: string): Promise<VoiceSessionDetail> {\n return request<VoiceSessionDetail>(\n `/sessions/${encodeURIComponent(sessionId)}`,\n 'GET',\n );\n },\n };\n}\n"],"mappings":"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","names":["VOICE_BASE","TOPIC_TRANSCRIPTION","TOPIC_CHAT","TOPIC_TOOL","TOPIC_CLIENT_TOOL","TOPIC_CLIENT_TOOL_RESULT","CLIENT_TOOL_RESULT_MAX_CHARS","ATTR_TRANSCRIPTION_FINAL","ATTR_SEGMENT_ID","ATTR_AGENT_STATE","QA_TRANSCRIPT_MAX_SEGMENTS","QA_TOOL_CALLS_MAX","request","path","method","body","headers","getConfig","res","errorMessage","errorCode","err","MindStudioInterfaceError","createListeners","mapAgentState","value","createVoiceClient","options","mintTimeToken","minted","lk","room","listeners","clientToolHandlers","state","muted","audioEl","qaHandle","setState","next","cb","emitError","error","cleanup","isAgent","identity","track","_pub","participant","_changed","reader","participantInfo","role","attributes","segmentId","text","final","chunk","invocation","respond","response","handler","result","serialized","parsed","status","abortHandler","name","session","event","callback","set","qaTranscript","idx","e","qaToolCalls","cursor","query","sessionId"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindstudio-ai/interface",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "description": "Frontend SDK for MindStudio v2 app interfaces",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",