@kognitivedev/backend-cloud 0.2.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/.turbo/turbo-build.log +2 -0
  2. package/.turbo/turbo-test.log +14 -0
  3. package/CHANGELOG.md +11 -0
  4. package/README.md +88 -0
  5. package/dist/cloud-voice-parameters.d.ts +11 -0
  6. package/dist/cloud-voice-parameters.js +219 -0
  7. package/dist/cloud-voice-prompt-service.d.ts +24 -0
  8. package/dist/cloud-voice-prompt-service.js +382 -0
  9. package/dist/cloud-voice-runtime-service.d.ts +73 -0
  10. package/dist/cloud-voice-runtime-service.js +443 -0
  11. package/dist/cloud-voice.d.ts +36 -0
  12. package/dist/cloud-voice.js +683 -0
  13. package/dist/index.d.ts +10 -0
  14. package/dist/index.js +26 -0
  15. package/dist/phone-control.d.ts +50 -0
  16. package/dist/phone-control.js +97 -0
  17. package/dist/phone-runtime/audio-playout-tracker.d.ts +51 -0
  18. package/dist/phone-runtime/audio-playout-tracker.js +93 -0
  19. package/dist/phone-runtime/openai-twilio-realtime.d.ts +95 -0
  20. package/dist/phone-runtime/openai-twilio-realtime.js +1074 -0
  21. package/dist/tools.d.ts +2 -0
  22. package/dist/tools.js +216 -0
  23. package/dist/types.d.ts +468 -0
  24. package/dist/types.js +2 -0
  25. package/dist/utils.d.ts +3 -0
  26. package/dist/utils.js +14 -0
  27. package/package.json +47 -0
  28. package/src/__tests__/audio-playout-tracker.test.ts +46 -0
  29. package/src/__tests__/cloud-voice.test.ts +1006 -0
  30. package/src/__tests__/openai-twilio-realtime.test.ts +1193 -0
  31. package/src/__tests__/phone-control.test.ts +105 -0
  32. package/src/cloud-voice-parameters.ts +236 -0
  33. package/src/cloud-voice-prompt-service.ts +493 -0
  34. package/src/cloud-voice-runtime-service.ts +465 -0
  35. package/src/cloud-voice.ts +831 -0
  36. package/src/index.ts +10 -0
  37. package/src/phone-control.ts +156 -0
  38. package/src/phone-runtime/audio-playout-tracker.ts +132 -0
  39. package/src/phone-runtime/openai-twilio-realtime.ts +1250 -0
  40. package/src/tools.ts +227 -0
  41. package/src/types.ts +529 -0
  42. package/src/utils.ts +11 -0
  43. package/tsconfig.json +13 -0
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ export * from "./cloud-voice";
2
+ export * from "./cloud-voice-parameters";
3
+ export * from "./cloud-voice-prompt-service";
4
+ export * from "./cloud-voice-runtime-service";
5
+ export * from "./phone-control";
6
+ export * from "./phone-runtime/audio-playout-tracker";
7
+ export * from "./phone-runtime/openai-twilio-realtime";
8
+ export * from "./tools";
9
+ export * from "./types";
10
+ export * from "./utils";
@@ -0,0 +1,156 @@
1
+ import { CLOUD_VOICE_PHONE_HANGUP_TOOL, CLOUD_VOICE_SIP_TRANSFER_TOOL } from "./cloud-voice";
2
+ import type { CloudVoiceSipControlAdapter, CloudVoicePhoneControlAdapter } from "./types";
3
+ import { getRecord, getString } from "./utils";
4
+
5
+ export interface ExecuteCloudVoicePhoneControlToolInput {
6
+ toolId: string;
7
+ args: unknown;
8
+ provider: string;
9
+ providerCallId: string;
10
+ adapters: CloudVoicePhoneControlAdapter[] | Record<string, CloudVoicePhoneControlAdapter>;
11
+ metadata?: Record<string, unknown>;
12
+ }
13
+
14
+ export interface ExecuteCloudVoicePhoneControlToolResult {
15
+ result: {
16
+ ok: true;
17
+ provider: string;
18
+ providerCallId: string;
19
+ status: string;
20
+ reason: string;
21
+ };
22
+ raw?: unknown;
23
+ }
24
+
25
+ export interface ExecuteCloudVoiceSipTransferToolInput {
26
+ toolId: string;
27
+ args: unknown;
28
+ provider: string;
29
+ providerCallId: string;
30
+ adapters: CloudVoiceSipControlAdapter[] | Record<string, CloudVoiceSipControlAdapter>;
31
+ resolveDestination?: (destinationId: string) => {
32
+ destination: Parameters<CloudVoiceSipControlAdapter["transferCall"]>[0]["destination"];
33
+ policy?: Parameters<CloudVoiceSipControlAdapter["transferCall"]>[0]["policy"];
34
+ } | null | Promise<{
35
+ destination: Parameters<CloudVoiceSipControlAdapter["transferCall"]>[0]["destination"];
36
+ policy?: Parameters<CloudVoiceSipControlAdapter["transferCall"]>[0]["policy"];
37
+ } | null>;
38
+ metadata?: Record<string, unknown>;
39
+ }
40
+
41
+ export interface ExecuteCloudVoiceSipTransferToolResult {
42
+ result: {
43
+ ok: true;
44
+ provider: string;
45
+ providerCallId: string;
46
+ status: string;
47
+ transferId?: string;
48
+ mode: string;
49
+ destinationId: string;
50
+ reason: string;
51
+ };
52
+ raw?: unknown;
53
+ }
54
+
55
+ export function getPhoneControlToolReason(args: unknown) {
56
+ return getString(getRecord(args).reason, "agent_requested");
57
+ }
58
+
59
+ function resolvePhoneControlAdapter(
60
+ adapters: CloudVoicePhoneControlAdapter[] | Record<string, CloudVoicePhoneControlAdapter>,
61
+ provider: string,
62
+ ) {
63
+ if (Array.isArray(adapters)) {
64
+ return adapters.find((adapter) => adapter.provider === provider) ?? null;
65
+ }
66
+ return adapters[provider] ?? null;
67
+ }
68
+
69
+ function resolveSipControlAdapter(
70
+ adapters: CloudVoiceSipControlAdapter[] | Record<string, CloudVoiceSipControlAdapter>,
71
+ provider: string,
72
+ ) {
73
+ if (Array.isArray(adapters)) {
74
+ return adapters.find((adapter) => adapter.provider === provider) ?? null;
75
+ }
76
+ return adapters[provider] ?? null;
77
+ }
78
+
79
+ export async function executeCloudVoicePhoneControlTool(
80
+ input: ExecuteCloudVoicePhoneControlToolInput,
81
+ ): Promise<ExecuteCloudVoicePhoneControlToolResult> {
82
+ if (input.toolId !== CLOUD_VOICE_PHONE_HANGUP_TOOL.name) {
83
+ throw new Error(`Unknown Cloud Voice phone control tool "${input.toolId}"`);
84
+ }
85
+ const providerCallId = getString(input.providerCallId, "");
86
+ if (!providerCallId) throw new Error("Cloud Voice phone control tool is missing providerCallId");
87
+
88
+ const adapter = resolvePhoneControlAdapter(input.adapters, input.provider);
89
+ if (!adapter) {
90
+ throw new Error(`Phone control tool "${input.toolId}" is not supported for provider "${input.provider}"`);
91
+ }
92
+
93
+ const reason = getPhoneControlToolReason(input.args);
94
+ const result = await adapter.hangUpCall({
95
+ providerCallId,
96
+ reason,
97
+ metadata: input.metadata,
98
+ });
99
+ return {
100
+ result: {
101
+ ok: true,
102
+ provider: result.provider,
103
+ providerCallId: result.providerCallId,
104
+ status: result.status,
105
+ reason,
106
+ },
107
+ raw: result.raw,
108
+ };
109
+ }
110
+
111
+ export async function executeCloudVoiceSipTransferTool(
112
+ input: ExecuteCloudVoiceSipTransferToolInput,
113
+ ): Promise<ExecuteCloudVoiceSipTransferToolResult> {
114
+ if (input.toolId !== CLOUD_VOICE_SIP_TRANSFER_TOOL.name) {
115
+ throw new Error(`Unknown Cloud Voice SIP control tool "${input.toolId}"`);
116
+ }
117
+ const providerCallId = getString(input.providerCallId, getString(getRecord(input.args).providerCallId, ""));
118
+ if (!providerCallId) throw new Error("Cloud Voice SIP transfer tool is missing providerCallId");
119
+
120
+ const adapter = resolveSipControlAdapter(input.adapters, input.provider);
121
+ if (!adapter) {
122
+ throw new Error(`SIP control tool "${input.toolId}" is not supported for provider "${input.provider}"`);
123
+ }
124
+
125
+ const args = getRecord(input.args);
126
+ const destinationId = getString(args.destinationId, "");
127
+ if (!destinationId) throw new Error("Cloud Voice SIP transfer tool is missing destinationId");
128
+ const resolved = await input.resolveDestination?.(destinationId);
129
+ if (!resolved) throw new Error(`Cloud Voice SIP transfer destination "${destinationId}" was not found`);
130
+ const mode = args.mode === "attended" || args.mode === "warm" || args.mode === "blind"
131
+ ? args.mode
132
+ : resolved.policy?.mode;
133
+ const reason = getString(args.reason, "agent_requested_transfer");
134
+
135
+ const result = await adapter.transferCall({
136
+ providerCallId,
137
+ destination: resolved.destination,
138
+ mode,
139
+ reason,
140
+ policy: resolved.policy,
141
+ metadata: input.metadata,
142
+ });
143
+ return {
144
+ result: {
145
+ ok: true,
146
+ provider: result.provider,
147
+ providerCallId: result.providerCallId,
148
+ status: result.status,
149
+ transferId: result.transferId,
150
+ mode: result.mode,
151
+ destinationId,
152
+ reason,
153
+ },
154
+ raw: result.raw,
155
+ };
156
+ }
@@ -0,0 +1,132 @@
1
+ export interface PhonePlayoutInterruption {
2
+ assistantItemId: string | null;
3
+ heardAudioMs: number;
4
+ queuedAudioMs: number;
5
+ pendingMarks: number;
6
+ interrupted: true;
7
+ clearedAt: string;
8
+ providerTruncationSupported: boolean;
9
+ }
10
+
11
+ export interface PhonePlayoutTrackerOptions {
12
+ frameDurationMs?: number;
13
+ }
14
+
15
+ export interface PhonePlayoutSnapshot {
16
+ assistantItemId: string | null;
17
+ queuedAudioMs: number;
18
+ heardAudioMs: number;
19
+ pendingMarks: number;
20
+ lastActivityAt: number;
21
+ }
22
+
23
+ export interface PhonePlayoutIdleResult extends PhonePlayoutSnapshot {
24
+ idle: boolean;
25
+ reason: "idle" | "timeout";
26
+ waitedMs: number;
27
+ }
28
+
29
+ export class PhonePlayoutTracker {
30
+ private readonly frameDurationMs: number;
31
+ private assistantItemId: string | null = null;
32
+ private queuedAudioMs = 0;
33
+ private heardAudioMs = 0;
34
+ private pendingMarks: string[] = [];
35
+ private lastActivityAt = Date.now();
36
+
37
+ constructor(options: PhonePlayoutTrackerOptions = {}) {
38
+ this.frameDurationMs = options.frameDurationMs ?? 20;
39
+ }
40
+
41
+ startAssistantItem(itemId: string | null) {
42
+ this.assistantItemId = itemId;
43
+ this.queuedAudioMs = 0;
44
+ this.heardAudioMs = 0;
45
+ this.pendingMarks = [];
46
+ this.lastActivityAt = Date.now();
47
+ }
48
+
49
+ recordOutboundFrame(input: { markName?: string | null; durationMs?: number; itemId?: string | null } = {}) {
50
+ if (input.itemId !== undefined && input.itemId !== this.assistantItemId) {
51
+ this.startAssistantItem(input.itemId);
52
+ }
53
+ this.queuedAudioMs += input.durationMs ?? this.frameDurationMs;
54
+ if (input.markName) this.pendingMarks.push(input.markName);
55
+ this.lastActivityAt = Date.now();
56
+ }
57
+
58
+ recordCarrierMark(markName?: string | null) {
59
+ if (this.pendingMarks.length === 0) return;
60
+ if (!markName) {
61
+ this.pendingMarks.shift();
62
+ this.heardAudioMs = Math.min(this.queuedAudioMs, this.heardAudioMs + this.frameDurationMs);
63
+ this.lastActivityAt = Date.now();
64
+ return;
65
+ }
66
+ const index = this.pendingMarks.indexOf(markName);
67
+ const consumed = index >= 0 ? index + 1 : 1;
68
+ this.pendingMarks.splice(0, consumed);
69
+ this.heardAudioMs = Math.min(this.queuedAudioMs, this.heardAudioMs + consumed * this.frameDurationMs);
70
+ this.lastActivityAt = Date.now();
71
+ }
72
+
73
+ interrupt(input: { elapsedMs?: number; providerTruncationSupported: boolean }): PhonePlayoutInterruption {
74
+ const heardAudioMs = Math.max(this.heardAudioMs, Math.min(input.elapsedMs ?? this.queuedAudioMs, this.queuedAudioMs));
75
+ const event: PhonePlayoutInterruption = {
76
+ assistantItemId: this.assistantItemId,
77
+ heardAudioMs,
78
+ queuedAudioMs: this.queuedAudioMs,
79
+ pendingMarks: this.pendingMarks.length,
80
+ interrupted: true,
81
+ clearedAt: new Date().toISOString(),
82
+ providerTruncationSupported: input.providerTruncationSupported,
83
+ };
84
+ this.reset();
85
+ return event;
86
+ }
87
+
88
+ reset() {
89
+ this.assistantItemId = null;
90
+ this.queuedAudioMs = 0;
91
+ this.heardAudioMs = 0;
92
+ this.pendingMarks = [];
93
+ this.lastActivityAt = Date.now();
94
+ }
95
+
96
+ snapshot(): PhonePlayoutSnapshot {
97
+ return {
98
+ assistantItemId: this.assistantItemId,
99
+ queuedAudioMs: this.queuedAudioMs,
100
+ heardAudioMs: this.heardAudioMs,
101
+ pendingMarks: this.pendingMarks.length,
102
+ lastActivityAt: this.lastActivityAt,
103
+ };
104
+ }
105
+
106
+ async waitForIdle(input: { timeoutMs?: number; quietMs?: number; pollMs?: number } = {}): Promise<PhonePlayoutIdleResult> {
107
+ const startedAt = Date.now();
108
+ const timeoutMs = Math.max(0, input.timeoutMs ?? 8_000);
109
+ const quietMs = Math.max(0, input.quietMs ?? 250);
110
+ const pollMs = Math.max(10, input.pollMs ?? 50);
111
+
112
+ while (Date.now() - startedAt < timeoutMs) {
113
+ const snapshot = this.snapshot();
114
+ if (snapshot.pendingMarks === 0 && Date.now() - snapshot.lastActivityAt >= quietMs) {
115
+ return {
116
+ ...snapshot,
117
+ idle: true,
118
+ reason: "idle",
119
+ waitedMs: Date.now() - startedAt,
120
+ };
121
+ }
122
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
123
+ }
124
+
125
+ return {
126
+ ...this.snapshot(),
127
+ idle: false,
128
+ reason: "timeout",
129
+ waitedMs: Date.now() - startedAt,
130
+ };
131
+ }
132
+ }