@inkibra/voice-sdk 0.1.1 → 0.2.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/README.md +115 -35
- package/dist/client.d.ts +18 -0
- package/dist/control.d.ts +77 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +804 -538
- package/dist/participant.d.ts +75 -0
- package/dist/public.d.ts +36 -110
- package/dist/server.d.ts +63 -60
- package/dist/server.js +655 -300
- package/dist/session-protocol.d.ts +103 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -1,75 +1,155 @@
|
|
|
1
1
|
# `@inkibra/voice-sdk`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Browser and server SDK, version 0.2. One audio owner per session; additional participants observe or execute tools without opening media. The Durable Object remains the sole conversation authority.
|
|
4
|
+
|
|
5
|
+
## Create, listen, then start
|
|
4
6
|
|
|
5
7
|
```ts
|
|
6
8
|
import LiveVoice from "@inkibra/voice-sdk";
|
|
7
9
|
|
|
8
|
-
const session = await LiveVoice.
|
|
10
|
+
const session = await LiveVoice.create({
|
|
9
11
|
token,
|
|
10
12
|
prompt: "Help the user complete a concise inventory.",
|
|
11
|
-
conversation: {
|
|
12
|
-
|
|
13
|
-
prompt: "Be concise and clarify uncertainty before moving on.",
|
|
14
|
-
},
|
|
15
|
-
tools: {
|
|
16
|
-
update_inventory: {
|
|
17
|
-
description: "Update an inventory item",
|
|
18
|
-
parameters: {
|
|
19
|
-
type: "object",
|
|
20
|
-
properties: { name: { type: "string" }, quantity: { type: "number" } },
|
|
21
|
-
required: ["name", "quantity"],
|
|
22
|
-
additionalProperties: false,
|
|
23
|
-
},
|
|
24
|
-
execute: async ({ name, quantity }) => updateInventory(String(name), Number(quantity)),
|
|
25
|
-
},
|
|
26
|
-
},
|
|
13
|
+
conversation: { starts: "assistant", prompt: "Clarify uncertainty before moving on." },
|
|
14
|
+
tools: browserTools,
|
|
27
15
|
});
|
|
28
16
|
|
|
29
17
|
session.on("transcript.instantaneous", ({ text }) => console.log(text));
|
|
30
18
|
session.on("transcript.canonical", ({ turns }) => console.log(turns));
|
|
31
19
|
session.on("floor", ({ owner, assistantAudible }) => console.log(owner, assistantAudible));
|
|
32
|
-
session.on("
|
|
33
|
-
|
|
34
|
-
renderAssistantForecast(predictions);
|
|
35
|
-
});
|
|
36
|
-
session.on("phase", ({ phase, previousPhase }) => console.log(phase, previousPhase));
|
|
37
|
-
session.on("tool.call", ({ name, input }) => console.log(name, input));
|
|
20
|
+
session.on("phase", ({ phase }) => console.log(phase));
|
|
21
|
+
session.on("task.update", task => renderTask(task));
|
|
38
22
|
session.on("error", console.error);
|
|
39
23
|
|
|
24
|
+
await session.start();
|
|
40
25
|
// Later:
|
|
41
|
-
session.stop();
|
|
26
|
+
await session.stop();
|
|
27
|
+
session.dispose();
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`create()` establishes control ownership and acknowledges tool registration. It does not open the microphone or begin the assistant opening. Await any backend attachments before `start()` if their tools must be available at the opening.
|
|
31
|
+
## Rate a completed session
|
|
32
|
+
|
|
33
|
+
Owners can submit one rating per rater kind after a call. Re-submitting replaces the previous rating for that session and rater:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
const result = await session.rate({
|
|
37
|
+
overall: 5,
|
|
38
|
+
scores: { waiting: 4, interruptions: 5, backchannels: 4, voice: 5, tools: 4 },
|
|
39
|
+
note: "Clear and responsive.",
|
|
40
|
+
});
|
|
41
|
+
console.log(result.storedFields);
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
The browser and server owner sessions use the owner session token already held by the SDK and derive the HTTPS control-plane origin from the realtime endpoint. Customer sessions retain only numeric scores and operational metadata; notes and moments are retained for private operator sessions.
|
|
45
|
+
|
|
46
|
+
Control and task events use WebSocket. Browser audio uses a native `RTCPeerConnection` with short-lived TURN credentials. Do not wrap the browser SDK in PCM capture/playback: native WebRTC playout is required for browser acoustic echo cancellation. Microphone capture requests mono, automatic gain control, echo cancellation, and noise suppression; effectiveness depends on browser/device support.
|
|
47
|
+
|
|
48
|
+
## Authorization
|
|
49
|
+
|
|
50
|
+
Create the owner token on your server; never put your Inkibra API key in browser code:
|
|
45
51
|
|
|
46
52
|
```ts
|
|
47
53
|
const response = await fetch("https://inkibra-voice.inkibra.workers.dev/v1/sessions", {
|
|
48
54
|
method: "POST",
|
|
49
55
|
headers: { authorization: `Bearer ${process.env.INKIBRA_API_KEY}`, "content-type": "application/json" },
|
|
50
|
-
body: JSON.stringify({
|
|
56
|
+
body: JSON.stringify({}),
|
|
57
|
+
});
|
|
58
|
+
if (!response.ok) throw new Error("Session authorization failed");
|
|
59
|
+
const { token, sessionId } = await response.json();
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Browser attachments use a separate token from `POST /v1/sessions/{sessionId}/attachment-tokens`, authenticated by the account's API key. Body: `{ participantId, observe: true, tools: ["search_docs"] }`. Tokens bind the account, session, participant, observation permission, allowed tool names, and expiry (15 minutes). `observe` defaults to `true`; `tools` defaults to `[]`. A session ID is not authorization. Do not share the owner token with observers or tools.
|
|
63
|
+
|
|
64
|
+
## Attach backend tools
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import LiveVoice from "@inkibra/voice-sdk/server";
|
|
68
|
+
|
|
69
|
+
const attachment = await LiveVoice.attach({
|
|
70
|
+
apiKey: process.env.INKIBRA_API_KEY!,
|
|
71
|
+
sessionId,
|
|
72
|
+
participantId: "research-backend",
|
|
73
|
+
tools: {
|
|
74
|
+
search_docs: {
|
|
75
|
+
description: "Search internal documentation",
|
|
76
|
+
parameters: {
|
|
77
|
+
type: "object",
|
|
78
|
+
properties: { query: { type: "string" } },
|
|
79
|
+
required: ["query"],
|
|
80
|
+
additionalProperties: false,
|
|
81
|
+
},
|
|
82
|
+
async execute({ query }, task) {
|
|
83
|
+
await task.progress({ message: "Searching documents", fraction: 0.1 });
|
|
84
|
+
const matches = await searchDocuments(String(query), { signal: task.signal });
|
|
85
|
+
await task.partial({ matches });
|
|
86
|
+
return { matches };
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
51
90
|
});
|
|
52
91
|
|
|
53
|
-
|
|
92
|
+
attachment.on("transcript.canonical", ({ turns }) => console.log(turns));
|
|
93
|
+
attachment.on("task.update", task => console.log(task));
|
|
94
|
+
// Disconnect only this participant:
|
|
95
|
+
await attachment.detach();
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The browser entry also has `attach({ token, sessionId, participantId, tools? })`. Neither attachment entry opens audio, changes the prompt, takes ownership, or stops the conversation. Tool names are unique across the session; collisions reject registration rather than overwrite another provider. Disconnected providers' tools are unavailable for new calls.
|
|
99
|
+
|
|
100
|
+
## Tasks and cancellation
|
|
101
|
+
|
|
102
|
+
Tools receive `execute(input, { taskId, sourceUserTurnId, signal, progress, partial })`. `sourceUserTurnId` is the optional canonical user-turn origin associated by the runtime with the task; it is separate from model-provided input and is absent after zero-data-retention clearing or an operational restart. Inputs, partials, and outputs must be JSON serializable. Progress updates status/UI; it does not instruct speech. Partial and final results enter model context through ordinary turn control, without forcing playback interruption. The conversation does not wait for the tool promise.
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
const tasks = await session.tasks.list();
|
|
106
|
+
const task = await session.tasks.get(taskId);
|
|
107
|
+
await session.tasks.cancel(taskId);
|
|
108
|
+
|
|
109
|
+
// Only the owning provider, after independently verifying unknown work:
|
|
110
|
+
await attachment.tasks.reconcile(taskId, { status: "succeeded", output: verifiedResult });
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Statuses: `queued`, `running`, `cancel_requested`, `succeeded`, `failed`, `cancelled`, `unknown`. The owner can inspect/cancel all tasks; each provider can inspect/cancel its own. An observer does not receive another provider's task payloads. Cancellation requests abort the handler's signal; they do not prove an external side effect was stopped or rolled back. Returning a value still reports success. Session shutdown requests cancellation without waiting indefinitely for external work.
|
|
114
|
+
|
|
115
|
+
## Reconnect and snapshots
|
|
116
|
+
|
|
117
|
+
Both handles expose `sessionId`, stable `participantId`, physical `connectionId`, and `snapshot`. `create()`, `attach()`, and `reconnect()` resolve after the authorized snapshot is synchronized, followed by ordered events. Catchup restores current state, not every missed interim token.
|
|
118
|
+
|
|
119
|
+
Connection events report `ready` once control is connected and synchronized; this does not mean media is running. The owner reports `live` after `start()` succeeds. `snapshot` events report synchronization and conversation-status changes.
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
await session.reconnect({ token: refreshedOwnerToken });
|
|
123
|
+
await attachment.reconnect({ token: refreshedAttachmentToken });
|
|
54
124
|
```
|
|
55
125
|
|
|
56
|
-
|
|
126
|
+
Omit `token` to reuse an unexpired credential. Reconnect the existing handle rather than creating a new session. Running handlers in that process continue; unacknowledged updates are resent and deduplicated. Old connections are fenced. Losing execution state makes delivered work `unknown`, never automatically reexecuted. Use `taskId` as an external idempotency key where supported; this is not an exactly-once side-effect guarantee.
|
|
127
|
+
|
|
128
|
+
Owner disconnection suspends audio for a 30-second grace period. Attachments cannot take over audio, and stale buffered audio is not replayed. After a Durable Object restart, the conversation fails closed. Delivered work without retained provider execution state becomes `unknown`; a surviving same-process handler may still report its operational outcome. Only opaque operational task metadata is persisted: prompts, transcripts, tool names/arguments/results, partials, and progress messages are not. Consequently `name`, `input`, and result fields can be unavailable after restart or conversation shutdown.
|
|
129
|
+
|
|
130
|
+
Within that grace period, reconnecting an already-started owner starts fresh media and restores canonical conversation text to the speech provider. It does not replay prior audio or restart background tools.
|
|
131
|
+
|
|
132
|
+
## Server audio owner
|
|
57
133
|
|
|
58
|
-
Telephone
|
|
134
|
+
Telephone/calling adapters use the same two-phase lifecycle:
|
|
59
135
|
|
|
60
136
|
```ts
|
|
61
137
|
import LiveVoice from "@inkibra/voice-sdk/server";
|
|
62
138
|
|
|
63
|
-
const session = await LiveVoice.
|
|
139
|
+
const session = await LiveVoice.create({
|
|
64
140
|
apiKey: process.env.INKIBRA_API_KEY!,
|
|
65
141
|
audio: providerAudioTransport,
|
|
66
|
-
voice: "ara",
|
|
67
142
|
prompt: "Help callers schedule an appointment.",
|
|
68
143
|
conversation: { starts: "assistant" },
|
|
69
|
-
tools: {},
|
|
70
144
|
});
|
|
71
|
-
|
|
145
|
+
await session.start();
|
|
72
146
|
await session.closed;
|
|
73
147
|
```
|
|
74
148
|
|
|
75
|
-
The
|
|
149
|
+
`audio` is PCM16LE mono at 24 kHz. The transport owns codec conversion and transient provider playback, not conversation state. Playback receipts report actual played samples and explicit completion/cancellation; queued bytes are not proof of playback. Supply `token` instead of `apiKey` when your backend issues owner tokens separately.
|
|
150
|
+
|
|
151
|
+
## Breaking migration from 0.1
|
|
152
|
+
|
|
153
|
+
- Replace `tool.call`/`tool.result` subscriptions with `task.update`; use one public `taskId`.
|
|
154
|
+
- Use `execute(input, task)` for progress, partial results, and cooperative cancellation.
|
|
155
|
+
- Await `stop()` for the owner or `detach()` for an attachment.
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ControlClient, type LiveControlCallbacks, type LiveSessionOptions } from "./control.js";
|
|
2
|
+
export type { LivePublicEvent, LiveSessionOpenOptions, LiveSessionOptions } from "./control.js";
|
|
3
|
+
export type LiveClientCallbacks = LiveControlCallbacks & {
|
|
4
|
+
onLevel(level: number): void;
|
|
5
|
+
onMediaStream?(track: "microphone" | "assistant", stream: MediaStream): void;
|
|
6
|
+
onAudioElement?(element: HTMLAudioElement): void;
|
|
7
|
+
onMediaClosed?(): void;
|
|
8
|
+
};
|
|
9
|
+
/** Browser media adapter over the shared control/task engine. */
|
|
10
|
+
export declare class LiveVoiceClient extends ControlClient {
|
|
11
|
+
private readonly mediaCallbacks;
|
|
12
|
+
private media?;
|
|
13
|
+
private mediaStarting?;
|
|
14
|
+
constructor(endpoint: string, mediaCallbacks: LiveClientCallbacks);
|
|
15
|
+
start(options?: LiveSessionOptions): Promise<void>;
|
|
16
|
+
closeSocket(reason?: string, terminal?: boolean): void;
|
|
17
|
+
private closeMedia;
|
|
18
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { LiveVoiceSnapshot, LiveVoiceTask, LiveVoiceTool, ToolContext } from "./session-protocol.js";
|
|
2
|
+
export type LivePublicEvent = {
|
|
3
|
+
name: string;
|
|
4
|
+
payload: Record<string, unknown>;
|
|
5
|
+
sequence?: number;
|
|
6
|
+
atMs?: number;
|
|
7
|
+
};
|
|
8
|
+
export type LiveControlCallbacks = {
|
|
9
|
+
onConnection(state: "connecting" | "ready" | "live" | "closed" | "error", detail?: string): void;
|
|
10
|
+
onPublicEvent(event: LivePublicEvent): void;
|
|
11
|
+
onSnapshot?(snapshot: LiveVoiceSnapshot): void;
|
|
12
|
+
onUnknown?(message: Record<string, unknown>): void;
|
|
13
|
+
onTaskExecute?(task: LiveVoiceTask, context: ToolContext): unknown | Promise<unknown>;
|
|
14
|
+
onTaskUpdate?(task: LiveVoiceTask): void;
|
|
15
|
+
onSessionLimit?(reason: string, durationSeconds?: number): void;
|
|
16
|
+
};
|
|
17
|
+
export type LiveSessionOpenOptions = {
|
|
18
|
+
token: string;
|
|
19
|
+
sessionId?: string;
|
|
20
|
+
participantId?: string;
|
|
21
|
+
mode: "create" | "attach" | "reconnect";
|
|
22
|
+
tools?: Record<string, LiveVoiceTool>;
|
|
23
|
+
developer?: unknown;
|
|
24
|
+
mediaMode?: "browser" | "server";
|
|
25
|
+
retainedTaskIds?: string[];
|
|
26
|
+
};
|
|
27
|
+
export type LiveSessionOptions = {
|
|
28
|
+
token?: string;
|
|
29
|
+
developer?: unknown;
|
|
30
|
+
mediaMode?: "browser" | "server";
|
|
31
|
+
};
|
|
32
|
+
/** Shared browser/server control, task execution, acknowledgement, and reconnect state. */
|
|
33
|
+
export declare class ControlClient {
|
|
34
|
+
protected endpoint: string;
|
|
35
|
+
protected callbacks: LiveControlCallbacks;
|
|
36
|
+
protected socket?: WebSocket;
|
|
37
|
+
protected sessionToken?: string;
|
|
38
|
+
private snapshotValue?;
|
|
39
|
+
private waits;
|
|
40
|
+
private executions;
|
|
41
|
+
private requestCounter;
|
|
42
|
+
private prepared;
|
|
43
|
+
private preparing?;
|
|
44
|
+
private starting?;
|
|
45
|
+
private started;
|
|
46
|
+
private terminal;
|
|
47
|
+
constructor(endpoint: string, callbacks: LiveControlCallbacks);
|
|
48
|
+
ratingUrl(sessionId: string): string;
|
|
49
|
+
setCallbacks(callbacks: LiveControlCallbacks): void;
|
|
50
|
+
get currentSnapshot(): LiveVoiceSnapshot | undefined;
|
|
51
|
+
retainedTaskIds(): string[];
|
|
52
|
+
open(options: LiveSessionOpenOptions): Promise<LiveVoiceSnapshot>;
|
|
53
|
+
prepare(): Promise<void>;
|
|
54
|
+
start(options?: LiveSessionOptions): Promise<void>;
|
|
55
|
+
stop(): Promise<void>;
|
|
56
|
+
detach(): Promise<void>;
|
|
57
|
+
closeSocket(reason?: string, terminal?: boolean): void;
|
|
58
|
+
dispose(): void;
|
|
59
|
+
reconnect(options: Omit<LiveSessionOpenOptions, "mode" | "token"> & {
|
|
60
|
+
token?: string;
|
|
61
|
+
}): Promise<LiveVoiceSnapshot>;
|
|
62
|
+
requestTask(operation: "list" | "get" | "cancel" | "reconcile", taskId?: string, resolution?: {
|
|
63
|
+
status: "succeeded" | "failed" | "cancelled";
|
|
64
|
+
output?: unknown;
|
|
65
|
+
error?: string;
|
|
66
|
+
}): Promise<LiveVoiceTask | LiveVoiceTask[] | undefined>;
|
|
67
|
+
playUiCue(cue: "recognition" | "committed"): void;
|
|
68
|
+
send(message: Record<string, unknown> | Uint8Array): void;
|
|
69
|
+
private exchange;
|
|
70
|
+
private onMessage;
|
|
71
|
+
private execute;
|
|
72
|
+
}
|
|
73
|
+
export declare function deferred<T>(): {
|
|
74
|
+
promise: Promise<T>;
|
|
75
|
+
resolve(value: T | PromiseLike<T>): void;
|
|
76
|
+
reject(reason?: unknown): void;
|
|
77
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import LiveVoice from "./public";
|
|
2
|
-
export * from "./public";
|
|
1
|
+
import LiveVoice from "./public.js";
|
|
2
|
+
export * from "./public.js";
|
|
3
3
|
export default LiveVoice;
|