@mindstudio-ai/interface 0.1.12 → 0.1.14
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/README.md +52 -1
- package/dist/chunk-5L3RRB4F.js +3 -0
- package/dist/chunk-5L3RRB4F.js.map +1 -0
- package/dist/index.js +3 -4
- package/dist/index.js.map +1 -1
- package/dist/voice.d.ts +154 -0
- package/dist/voice.js +2 -0
- package/dist/voice.js.map +1 -0
- package/package.json +9 -1
package/dist/voice.d.ts
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Voice client for apps with a voice interface.
|
|
3
|
+
*
|
|
4
|
+
* Imported from the `./voice` subpath so apps that never use voice ship none
|
|
5
|
+
* of it — and `livekit-client` (the one dependency, for the realtime media
|
|
6
|
+
* transport) is loaded dynamically on the first `startSession()` call:
|
|
7
|
+
*
|
|
8
|
+
* ```
|
|
9
|
+
* import { createVoiceClient } from '@mindstudio-ai/interface/voice';
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* Flow: `startSession()` mints a session from the platform
|
|
13
|
+
* (`POST /_/voice/sessions`), connects to the media room, publishes the
|
|
14
|
+
* microphone, and returns a {@link VoiceSession} that surfaces the
|
|
15
|
+
* conversation as events — agent state, live captions for both sides, tool
|
|
16
|
+
* activity — plus mute / text-injection / end controls. Agent audio playback
|
|
17
|
+
* is handled internally (a hidden autoplaying element); the app never touches
|
|
18
|
+
* audio elements.
|
|
19
|
+
*/
|
|
20
|
+
/** Lifecycle of a live voice session, driven by the agent's published state. */
|
|
21
|
+
type VoiceSessionState = 'connecting' | 'listening' | 'thinking' | 'speaking' | 'ended';
|
|
22
|
+
/** One live-caption update. */
|
|
23
|
+
interface VoiceTranscriptEvent {
|
|
24
|
+
/** Whose speech this is. */
|
|
25
|
+
role: 'user' | 'agent';
|
|
26
|
+
/** Stable id for one utterance — updates with the same id replace, not append. */
|
|
27
|
+
segmentId: string;
|
|
28
|
+
/** The utterance text so far (always the full text, never a delta). */
|
|
29
|
+
text: string;
|
|
30
|
+
/** True once this segment won't change again. */
|
|
31
|
+
final: boolean;
|
|
32
|
+
}
|
|
33
|
+
/** Tool activity from the agent, for inline status UI. */
|
|
34
|
+
interface VoiceToolCallEvent {
|
|
35
|
+
/** The method id being executed. */
|
|
36
|
+
method: string;
|
|
37
|
+
status: 'running' | 'done' | 'failed';
|
|
38
|
+
/** Milliseconds since epoch. */
|
|
39
|
+
at: number;
|
|
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.
|
|
45
|
+
*/
|
|
46
|
+
result?: unknown;
|
|
47
|
+
/** Set instead of `result` when the payload exceeded the forwarding size
|
|
48
|
+
* cap (~32KB serialized) — fetch the data yourself in that case. */
|
|
49
|
+
resultTruncated?: boolean;
|
|
50
|
+
}
|
|
51
|
+
interface VoiceSessionCallbacks {
|
|
52
|
+
stateChange: (state: VoiceSessionState) => void;
|
|
53
|
+
transcript: (event: VoiceTranscriptEvent) => void;
|
|
54
|
+
toolCall: (event: VoiceToolCallEvent) => void;
|
|
55
|
+
error: (error: Error) => void;
|
|
56
|
+
}
|
|
57
|
+
interface StartSessionOptions {
|
|
58
|
+
/** Abort the connection attempt (the promise rejects with `AbortError`). */
|
|
59
|
+
signal?: AbortSignal;
|
|
60
|
+
}
|
|
61
|
+
/** A live voice conversation. */
|
|
62
|
+
interface VoiceSession {
|
|
63
|
+
/** The platform session id (matches the call record in session history). */
|
|
64
|
+
readonly sessionId: string;
|
|
65
|
+
/** The media room name. */
|
|
66
|
+
readonly room: string;
|
|
67
|
+
/** Current session state. */
|
|
68
|
+
readonly state: VoiceSessionState;
|
|
69
|
+
/** Whether the microphone is currently muted. */
|
|
70
|
+
readonly isMuted: boolean;
|
|
71
|
+
/** Subscribe to a session event. Returns an unsubscribe function. */
|
|
72
|
+
on<K extends keyof VoiceSessionCallbacks>(event: K, callback: VoiceSessionCallbacks[K]): () => void;
|
|
73
|
+
/** Mute the microphone (the session stays live). */
|
|
74
|
+
mute(): Promise<void>;
|
|
75
|
+
/** Unmute the microphone. */
|
|
76
|
+
unmute(): Promise<void>;
|
|
77
|
+
/**
|
|
78
|
+
* Inject text into the live conversation — the agent treats it like user
|
|
79
|
+
* speech (interrupts what it's saying and responds). Use for exact strings
|
|
80
|
+
* that are painful aloud: addresses, codes, emails.
|
|
81
|
+
*/
|
|
82
|
+
sendText(text: string): Promise<void>;
|
|
83
|
+
/** End the session and release the microphone. */
|
|
84
|
+
end(): Promise<void>;
|
|
85
|
+
}
|
|
86
|
+
/** A past voice session (call record), transcript excluded. */
|
|
87
|
+
interface VoiceSessionSummary {
|
|
88
|
+
id: string;
|
|
89
|
+
status: 'active' | 'ended' | 'failed';
|
|
90
|
+
startedAt: string;
|
|
91
|
+
endedAt: string | null;
|
|
92
|
+
durationSecs: number | null;
|
|
93
|
+
endedReason: string | null;
|
|
94
|
+
}
|
|
95
|
+
/** One persisted transcript entry on a call record. */
|
|
96
|
+
interface VoiceTranscriptEntry {
|
|
97
|
+
role: 'user' | 'assistant';
|
|
98
|
+
text: string;
|
|
99
|
+
interrupted?: boolean;
|
|
100
|
+
/** Milliseconds since epoch. */
|
|
101
|
+
at: number;
|
|
102
|
+
}
|
|
103
|
+
/** A past voice session with its transcript. */
|
|
104
|
+
interface VoiceSessionDetail extends VoiceSessionSummary {
|
|
105
|
+
transcript: VoiceTranscriptEntry[];
|
|
106
|
+
}
|
|
107
|
+
interface VoiceSessionListPage {
|
|
108
|
+
sessions: VoiceSessionSummary[];
|
|
109
|
+
nextCursor: string | null;
|
|
110
|
+
}
|
|
111
|
+
interface VoiceClient {
|
|
112
|
+
/**
|
|
113
|
+
* Start a voice session: mints it from the platform, connects to the media
|
|
114
|
+
* room, and publishes the microphone.
|
|
115
|
+
*
|
|
116
|
+
* @throws {MindStudioInterfaceError} `microphone_denied` when the user
|
|
117
|
+
* refuses microphone access, `voice_concurrency_limit` /
|
|
118
|
+
* `voice_visitor_limit` when the app's session limits are reached,
|
|
119
|
+
* `auth_required` (401) / `role_required` (403) when the interface's
|
|
120
|
+
* auth block denies the caller (route to the app's login flow),
|
|
121
|
+
* `no_voice_config` when the app has no voice interface.
|
|
122
|
+
*/
|
|
123
|
+
startSession(options?: StartSessionOptions): Promise<VoiceSession>;
|
|
124
|
+
/** List the current user's (or visitor's) past sessions, newest first. */
|
|
125
|
+
listSessions(cursor?: string): Promise<VoiceSessionListPage>;
|
|
126
|
+
/** Fetch one past session, transcript included. */
|
|
127
|
+
getSession(sessionId: string): Promise<VoiceSessionDetail>;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Create a voice client.
|
|
131
|
+
*
|
|
132
|
+
* @returns A {@link VoiceClient} for starting live sessions and reading past
|
|
133
|
+
* call records.
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* ```ts
|
|
137
|
+
* import { createVoiceClient } from '@mindstudio-ai/interface/voice';
|
|
138
|
+
*
|
|
139
|
+
* const voice = createVoiceClient();
|
|
140
|
+
* const session = await voice.startSession();
|
|
141
|
+
*
|
|
142
|
+
* session.on('stateChange', (state) => setOrbState(state));
|
|
143
|
+
* session.on('transcript', ({ role, segmentId, text, final }) =>
|
|
144
|
+
* upsertCaption(segmentId, role, text, final),
|
|
145
|
+
* );
|
|
146
|
+
* session.on('toolCall', ({ method, status }) => showToolStatus(method, status));
|
|
147
|
+
*
|
|
148
|
+
* // ... later
|
|
149
|
+
* await session.end();
|
|
150
|
+
* ```
|
|
151
|
+
*/
|
|
152
|
+
declare function createVoiceClient(): VoiceClient;
|
|
153
|
+
|
|
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 };
|
package/dist/voice.js
ADDED
|
@@ -0,0 +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};
|
|
2
|
+
//# sourceMappingURL=voice.js.map
|
|
@@ -0,0 +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":"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","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"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mindstudio-ai/interface",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"description": "Frontend SDK for MindStudio v2 app interfaces",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
"types": "./dist/index.d.ts",
|
|
11
11
|
"import": "./dist/index.js",
|
|
12
12
|
"default": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./voice": {
|
|
15
|
+
"types": "./dist/voice.d.ts",
|
|
16
|
+
"import": "./dist/voice.js",
|
|
17
|
+
"default": "./dist/voice.js"
|
|
13
18
|
}
|
|
14
19
|
},
|
|
15
20
|
"files": [
|
|
@@ -55,5 +60,8 @@
|
|
|
55
60
|
},
|
|
56
61
|
"lint-staged": {
|
|
57
62
|
"**/*": "prettier --write --ignore-unknown"
|
|
63
|
+
},
|
|
64
|
+
"dependencies": {
|
|
65
|
+
"livekit-client": "^2.22.0"
|
|
58
66
|
}
|
|
59
67
|
}
|