supafone-labs 0.4.14 → 0.5.2

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/src/index.ts CHANGED
@@ -287,16 +287,38 @@ export interface LabsVoiceProviderTraits {
287
287
  is_owner?: boolean | null;
288
288
  }
289
289
 
290
+ /** Stable provider presentation metadata returned by the hosted catalog. */
291
+ export interface LabsVoiceProviderBrand {
292
+ /** Canonical provider key used for filtering and grouping. */
293
+ key: string;
294
+ /** Human-readable provider/company name. */
295
+ name: string;
296
+ /** Stable local asset slug when Supafone has an official provider mark. */
297
+ logo_slug?: string | null;
298
+ /** Absolute, browser-ready provider logo URL. */
299
+ logo_url?: string | null;
300
+ /** Provider's official product or company website. */
301
+ website_url?: string | null;
302
+ }
303
+
290
304
  export interface LabsVoiceCatalogItem {
291
305
  /** Stable runtime reference, including provider prefix when required. */
292
306
  id: string;
293
307
  voice_id: string;
294
308
  /** Human-readable provider display name. */
295
309
  provider: string;
310
+ /** Canonical brand display name for the synthesis provider. */
311
+ provider_label?: string;
312
+ /** Browser-ready official provider mark, or null for an unknown provider. */
313
+ provider_logo_url?: string | null;
314
+ /** Branding for the provider that renders the voice. */
315
+ provider_brand?: LabsVoiceProviderBrand;
296
316
  /** Canonical SDK/runtime provider key. */
297
317
  provider_key: string;
298
318
  /** Runtime adapter used to place the voice on an Ultravox call. */
299
319
  runtime_provider_key: string;
320
+ /** Branding for the realtime runtime, which can differ from synthesis. */
321
+ runtime_provider_brand?: LabsVoiceProviderBrand;
300
322
  /** Actual TTS engine behind the voice (can differ for Ultravox built-ins). */
301
323
  synthesis_provider_key: string;
302
324
  /** Raw identifier used by the provider API. */
@@ -662,6 +684,192 @@ export interface LabsUltravoxRuntime {
662
684
  [extra: string]: unknown;
663
685
  }
664
686
 
687
+ export interface OutboundCallModeObservability {
688
+ enabled?: boolean;
689
+ includeTrigger?: boolean;
690
+ includeTransitions?: boolean;
691
+ includeTerminationReason?: boolean;
692
+ metadata?: Record<string, unknown>;
693
+ }
694
+
695
+ /** Public, provider-neutral controls for temporary outbound IVR navigation. */
696
+ export interface OutboundCallModeConfig {
697
+ enabled?: boolean;
698
+ autoDetect?: boolean;
699
+ dtmfToolEnabled?: boolean;
700
+ maxDurationSeconds?: number;
701
+ maxKeypresses?: number;
702
+ repeatedMenuLimit?: number;
703
+ noProgressTimeoutSeconds?: number;
704
+ humanDetectionEnabled?: boolean;
705
+ resumeOnHuman?: boolean;
706
+ observability?: OutboundCallModeObservability;
707
+ }
708
+
709
+ /** Capabilities positively reported by the active telephony/runtime adapter. */
710
+ export interface OutboundCallModeCapabilities {
711
+ provider?: string;
712
+ dtmf?: boolean;
713
+ statePersistence?: boolean;
714
+ humanDetection?: boolean;
715
+ observability?: boolean;
716
+ metadata?: Record<string, unknown>;
717
+ }
718
+
719
+ export interface OutboundCallModeReadiness {
720
+ ready: boolean;
721
+ status: "disabled" | "ready" | "unknown" | "unsupported";
722
+ provider: string;
723
+ transportFamily: string;
724
+ requiredCapabilities: string[];
725
+ missingCapabilities: string[];
726
+ unsupportedCapabilities: string[];
727
+ reasons: string[];
728
+ }
729
+
730
+ export const OUTBOUND_CALL_MODE_BOUNDS = {
731
+ maxDurationSeconds: { default: 180, min: 30, max: 900 },
732
+ maxKeypresses: { default: 12, min: 1, max: 64 },
733
+ repeatedMenuLimit: { default: 3, min: 1, max: 10 },
734
+ noProgressTimeoutSeconds: { default: 30, min: 5, max: 120 },
735
+ } as const;
736
+
737
+ export const OUTBOUND_CALL_MODE_DEFAULTS = {
738
+ enabled: false,
739
+ autoDetect: true,
740
+ dtmfToolEnabled: true,
741
+ maxDurationSeconds: 180,
742
+ maxKeypresses: 12,
743
+ repeatedMenuLimit: 3,
744
+ noProgressTimeoutSeconds: 30,
745
+ humanDetectionEnabled: true,
746
+ resumeOnHuman: true,
747
+ observability: {
748
+ enabled: true,
749
+ includeTrigger: true,
750
+ includeTransitions: true,
751
+ includeTerminationReason: true,
752
+ },
753
+ } as const;
754
+
755
+ export interface OutboundCallModeProviderProfile {
756
+ contractSupported: boolean;
757
+ execution: "adapter_runtime_dependent";
758
+ capabilityPolicy: "fail_closed";
759
+ transportFamily: string;
760
+ providers: readonly string[];
761
+ requiredCapabilities: readonly string[];
762
+ }
763
+
764
+ const OUTBOUND_PROVIDER_PROFILE_BASE = {
765
+ contractSupported: true,
766
+ execution: "adapter_runtime_dependent",
767
+ capabilityPolicy: "fail_closed",
768
+ requiredCapabilities: ["dtmf", "state_persistence", "human_detection"],
769
+ } as const;
770
+
771
+ export const OUTBOUND_CALL_MODE_PROVIDER_MATRIX: Record<
772
+ string,
773
+ OutboundCallModeProviderProfile
774
+ > = {
775
+ supafoneManaged: {
776
+ ...OUTBOUND_PROVIDER_PROFILE_BASE,
777
+ transportFamily: "supafone_managed",
778
+ providers: ["supafone", "supafone_managed", "managed"],
779
+ },
780
+ twilio: {
781
+ ...OUTBOUND_PROVIDER_PROFILE_BASE,
782
+ transportFamily: "twilio",
783
+ providers: ["twilio", "byo_twilio"],
784
+ },
785
+ telnyx: {
786
+ ...OUTBOUND_PROVIDER_PROFILE_BASE,
787
+ transportFamily: "telnyx",
788
+ providers: ["telnyx", "byo_telnyx"],
789
+ },
790
+ plivo: {
791
+ ...OUTBOUND_PROVIDER_PROFILE_BASE,
792
+ transportFamily: "plivo",
793
+ providers: ["plivo", "byo_plivo"],
794
+ },
795
+ signalWire: {
796
+ ...OUTBOUND_PROVIDER_PROFILE_BASE,
797
+ transportFamily: "signalwire",
798
+ providers: ["signalwire", "signal_wire", "byo_signalwire"],
799
+ },
800
+ sipByoc: {
801
+ ...OUTBOUND_PROVIDER_PROFILE_BASE,
802
+ transportFamily: "sip_byoc",
803
+ providers: ["sip", "custom_sip", "byo_sip", "byoc", "sip_byoc"],
804
+ },
805
+ };
806
+
807
+ export function outboundCallModeProviderProfile(provider: string): OutboundCallModeProviderProfile {
808
+ const normalized = String(provider ?? "").trim().toLowerCase();
809
+ for (const profile of Object.values(OUTBOUND_CALL_MODE_PROVIDER_MATRIX)) {
810
+ if (profile.providers.includes(normalized)) {
811
+ return { ...profile, providers: [...profile.providers] };
812
+ }
813
+ }
814
+ return {
815
+ contractSupported: false,
816
+ execution: "adapter_runtime_dependent",
817
+ capabilityPolicy: "fail_closed",
818
+ transportFamily: "unknown",
819
+ providers: normalized ? [normalized] : [],
820
+ requiredCapabilities: ["dtmf", "state_persistence", "human_detection"],
821
+ };
822
+ }
823
+
824
+ export function outboundCallModeReadiness(
825
+ config?: OutboundCallModeConfig,
826
+ capabilities?: OutboundCallModeCapabilities,
827
+ ): OutboundCallModeReadiness {
828
+ const provider = String(capabilities?.provider ?? "");
829
+ const profile = outboundCallModeProviderProfile(provider);
830
+ const normalized = outboundCallModePayload(config ?? {});
831
+ if (normalized.enabled !== true) {
832
+ return {
833
+ ready: true,
834
+ status: "disabled",
835
+ provider,
836
+ transportFamily: profile.transportFamily,
837
+ requiredCapabilities: [],
838
+ missingCapabilities: [],
839
+ unsupportedCapabilities: [],
840
+ reasons: [],
841
+ };
842
+ }
843
+
844
+ const requiredCapabilities = [...(normalized.required_capabilities as string[])];
845
+ const reported: Record<string, boolean | undefined> = {
846
+ dtmf: capabilities?.dtmf,
847
+ state_persistence: capabilities?.statePersistence,
848
+ human_detection: capabilities?.humanDetection,
849
+ observability: capabilities?.observability,
850
+ };
851
+ const missingCapabilities = requiredCapabilities.filter((key) => reported[key] === undefined);
852
+ const unsupportedCapabilities = requiredCapabilities.filter((key) => reported[key] === false);
853
+ const status = unsupportedCapabilities.length
854
+ ? "unsupported"
855
+ : missingCapabilities.length
856
+ ? "unknown"
857
+ : "ready";
858
+ const reasons = unsupportedCapabilities.length
859
+ ? unsupportedCapabilities.map((key) => `Adapter reports ${key} is unsupported.`)
860
+ : missingCapabilities.map((key) => `Adapter did not report ${key} support.`);
861
+ return {
862
+ ready: status === "ready",
863
+ status,
864
+ provider,
865
+ transportFamily: profile.transportFamily,
866
+ requiredCapabilities,
867
+ missingCapabilities,
868
+ unsupportedCapabilities,
869
+ reasons,
870
+ };
871
+ }
872
+
665
873
  export interface CreateLabsAgentRequest {
666
874
  agencyId?: string;
667
875
  agency_id?: string;
@@ -746,6 +954,9 @@ export interface CreateLabsAgentRequest {
746
954
  voice_watcher?: boolean;
747
955
  voiceWatcherModel?: string;
748
956
  voice_watcher_model?: string;
957
+ /** Opt-in outbound IVR mode. Execution depends on adapter-reported readiness. */
958
+ outboundCallMode?: OutboundCallModeConfig;
959
+ outbound_call_mode?: OutboundCallModeConfig;
749
960
  metadata?: Record<string, unknown>;
750
961
  }
751
962
 
@@ -772,6 +983,49 @@ export interface DeleteLabsAgentResponse {
772
983
  [extra: string]: unknown;
773
984
  }
774
985
 
986
+ export type LabsAgentLifecycle = "draft" | "active" | "paused" | "archived" | string;
987
+
988
+ export interface LabsAgentReadiness {
989
+ ready: boolean;
990
+ status?: string;
991
+ checks?: Array<Record<string, unknown>>;
992
+ [extra: string]: unknown;
993
+ }
994
+
995
+ export type UpdateLabsAgentRequest = Partial<CreateLabsAgentRequest>;
996
+
997
+ export interface LabsAgentLifecycleResponse {
998
+ success?: boolean;
999
+ agent?: LabsAgentResponse;
1000
+ lifecycle?: LabsAgentLifecycle;
1001
+ readiness?: LabsAgentReadiness;
1002
+ [extra: string]: unknown;
1003
+ }
1004
+
1005
+ export interface LabsKnowledgeSyncRequest {
1006
+ websiteUrl?: string;
1007
+ website_url?: string;
1008
+ url?: string;
1009
+ }
1010
+
1011
+ export interface LabsKnowledgeSyncResponse {
1012
+ agent: LabsAgentResponse;
1013
+ chunks_indexed?: number;
1014
+ scraped_ok?: boolean;
1015
+ last_error?: string | null;
1016
+ website_detached?: boolean;
1017
+ }
1018
+
1019
+ export interface LabsKnowledgeChatTurn {
1020
+ role: "user" | "assistant";
1021
+ content: string;
1022
+ }
1023
+
1024
+ export interface LabsKnowledgeChatResponse {
1025
+ reply: string;
1026
+ sources: string[];
1027
+ }
1028
+
775
1029
  export interface LabsCapabilitiesResponse {
776
1030
  product: string;
777
1031
  api_namespace: string;
@@ -826,7 +1080,10 @@ export interface LabsVoiceListResponse {
826
1080
  configured: boolean;
827
1081
  voice_count: number;
828
1082
  error?: string | null;
1083
+ brand?: LabsVoiceProviderBrand;
829
1084
  }>;
1085
+ /** Distinct synthesis-provider brands represented in the complete result. */
1086
+ provider_brands?: LabsVoiceProviderBrand[];
830
1087
  provider_accounts?: Record<string, unknown>;
831
1088
  errors?: Record<string, string>;
832
1089
  catalog?: {
@@ -1151,6 +1408,8 @@ export interface LabsAgentResponse {
1151
1408
  display_name?: string;
1152
1409
  runtime_mode?: string;
1153
1410
  preset_key?: string;
1411
+ status?: string;
1412
+ lifecycle?: LabsAgentLifecycle;
1154
1413
  profile?: Record<string, unknown>;
1155
1414
  runtime?: Record<string, unknown>;
1156
1415
  [extra: string]: unknown;
@@ -1186,6 +1445,9 @@ export interface ListLabsAgentsResponse {
1186
1445
 
1187
1446
  export interface GetLabsAgentResponse {
1188
1447
  agent: LabsAgentResponse;
1448
+ runtime?: Record<string, unknown>;
1449
+ widget?: Record<string, unknown>;
1450
+ readiness?: LabsAgentReadiness;
1189
1451
  }
1190
1452
 
1191
1453
  export interface STTResult {
@@ -1729,6 +1991,37 @@ export class SupafoneLabs {
1729
1991
  }
1730
1992
  }
1731
1993
 
1994
+ /** @internal Multipart upload to the Supafone product API with the Labs key. */
1995
+ async requestSupafoneUpload<T>(
1996
+ path: string,
1997
+ file: Uint8Array | ArrayBuffer | Blob,
1998
+ filename: string,
1999
+ ): Promise<T> {
2000
+ const ctrl = new AbortController();
2001
+ const timer = setTimeout(() => ctrl.abort(), Math.max(this.timeoutMs, 120_000));
2002
+ try {
2003
+ const safeFilename = filename.replace(/["\\\r\n]/g, "").slice(0, 120) || "document.txt";
2004
+ const blob = file instanceof Blob ? file : new Blob([file as BlobPart]);
2005
+ const form = new FormData();
2006
+ form.append("file", blob, safeFilename);
2007
+ const res = await fetch(this.supafoneApiBaseUrl + path, {
2008
+ method: "POST",
2009
+ signal: ctrl.signal,
2010
+ headers: { Authorization: `Bearer ${this.supafoneApiKey}` },
2011
+ body: form,
2012
+ });
2013
+ const text = await res.text();
2014
+ const parsed = text ? safeJson(text) : {};
2015
+ if (!res.ok) {
2016
+ const detail = (parsed as { detail?: string })?.detail ?? text ?? `HTTP ${res.status}`;
2017
+ throw new SupafoneLabsError(`POST ${path}: ${detail}`, res.status, parsed);
2018
+ }
2019
+ return parsed as T;
2020
+ } finally {
2021
+ clearTimeout(timer);
2022
+ }
2023
+ }
2024
+
1732
2025
  /** @internal Authenticated binary request to the Supafone app API. */
1733
2026
  async requestSupafoneBinary(path: string): Promise<LabsVoicePreview> {
1734
2027
  const ctrl = new AbortController();
@@ -2371,6 +2664,15 @@ export interface CampaignLiveView {
2371
2664
  stats?: Record<string, unknown> | null;
2372
2665
  }
2373
2666
 
2667
+ export interface CampaignCreateInput {
2668
+ name?: string;
2669
+ goal?: string;
2670
+ agentId?: string;
2671
+ accountId?: string;
2672
+ settings?: Record<string, unknown>;
2673
+ outboundCallMode?: OutboundCallModeConfig;
2674
+ }
2675
+
2374
2676
  export interface CampaignUpdateInput {
2375
2677
  name?: string;
2376
2678
  goal?: string;
@@ -2379,6 +2681,7 @@ export interface CampaignUpdateInput {
2379
2681
  emailBody?: string;
2380
2682
  cadence?: { channel: "voice" | "email"; delay_hours: number }[];
2381
2683
  settings?: Record<string, unknown>;
2684
+ outboundCallMode?: OutboundCallModeConfig;
2382
2685
  }
2383
2686
 
2384
2687
  export interface BrandScanResult {
@@ -2443,13 +2746,21 @@ class CampaignsNamespace {
2443
2746
  return this.sm.requestAccountApi("GET", `/api/v1/campaigns${query}`);
2444
2747
  }
2445
2748
 
2446
- create(opts: { name?: string; goal?: string; agentId?: string; accountId?: string } = {}): Promise<{ campaign: CampaignSummary }> {
2447
- return this.sm.requestAccountApi("POST", "/api/v1/campaigns", compact({
2749
+ async create(opts: CampaignCreateInput = {}): Promise<{ campaign: CampaignSummary }> {
2750
+ const created = await this.sm.requestAccountApi<{ campaign: CampaignSummary }>("POST", "/api/v1/campaigns", compact({
2448
2751
  name: opts.name ?? "New campaign",
2449
2752
  goal: opts.goal ?? "book",
2450
2753
  agent_id: opts.agentId,
2451
2754
  account_id: opts.accountId,
2452
2755
  }));
2756
+ const settings = campaignSettingsPayload(opts.settings, opts.outboundCallMode);
2757
+ if (!Object.keys(settings).length) return created;
2758
+ if (!created.campaign?.id) {
2759
+ throw new SupafoneLabsError(
2760
+ "Campaign was created but no campaign id was returned; outbound call mode was not saved.",
2761
+ );
2762
+ }
2763
+ return this.update(created.campaign.id, { settings });
2453
2764
  }
2454
2765
 
2455
2766
  get(campaignId: string): Promise<{ campaign: CampaignSummary }> {
@@ -2457,6 +2768,7 @@ class CampaignsNamespace {
2457
2768
  }
2458
2769
 
2459
2770
  update(campaignId: string, input: CampaignUpdateInput): Promise<{ campaign: CampaignSummary }> {
2771
+ const settings = campaignSettingsPayload(input.settings, input.outboundCallMode);
2460
2772
  const payload = compact({
2461
2773
  name: input.name,
2462
2774
  goal: input.goal,
@@ -2464,10 +2776,12 @@ class CampaignsNamespace {
2464
2776
  email_subject: input.emailSubject,
2465
2777
  email_body: input.emailBody,
2466
2778
  cadence: input.cadence,
2467
- settings: input.settings,
2779
+ settings: Object.keys(settings).length ? settings : undefined,
2468
2780
  });
2469
2781
  if (!Object.keys(payload as Record<string, unknown>).length) {
2470
- throw new SupafoneLabsError("Nothing to update — pass name, goal, agentId, emailSubject, emailBody, cadence, or settings");
2782
+ throw new SupafoneLabsError(
2783
+ "Nothing to update — pass name, goal, agentId, emailSubject, emailBody, cadence, settings, or outboundCallMode",
2784
+ );
2471
2785
  }
2472
2786
  return this.sm.requestAccountApi("PUT", `/api/v1/campaigns/${encodeURIComponent(campaignId)}`, payload);
2473
2787
  }
@@ -2903,6 +3217,131 @@ class LabsAgentsNamespace {
2903
3217
  );
2904
3218
  }
2905
3219
 
3220
+ /** Update an existing durable agent without changing omitted fields. */
3221
+ update(
3222
+ agentKey: string,
3223
+ input: UpdateLabsAgentRequest,
3224
+ opts: GetLabsAgentOptions = {},
3225
+ ): Promise<LabsAgentLifecycleResponse> {
3226
+ const q = new URLSearchParams();
3227
+ if (opts.agencyId) q.set("agency_id", opts.agencyId);
3228
+ const suffix = q.toString() ? `?${q}` : "";
3229
+ return this.sm.requestSupafoneApi<LabsAgentLifecycleResponse>(
3230
+ "PATCH",
3231
+ `/api/v1/labs/agents/${encodeURIComponent(agentKey)}${suffix}`,
3232
+ labsAgentUpdatePayload(input),
3233
+ );
3234
+ }
3235
+
3236
+ /** Scrape a website and rebuild this agent's managed knowledge corpus. */
3237
+ syncKnowledge(
3238
+ agentId: string,
3239
+ input: LabsKnowledgeSyncRequest = {},
3240
+ ): Promise<LabsKnowledgeSyncResponse> {
3241
+ return this.sm.requestSupafoneApi<LabsKnowledgeSyncResponse>(
3242
+ "POST",
3243
+ `/api/v1/agents/${encodeURIComponent(agentId)}/sync-knowledge`,
3244
+ { url: input.url ?? input.website_url ?? input.websiteUrl ?? "" },
3245
+ );
3246
+ }
3247
+
3248
+ /** Remove website knowledge while preserving uploaded documents and notes. */
3249
+ detachWebsiteKnowledge(agentId: string): Promise<LabsKnowledgeSyncResponse> {
3250
+ return this.sm.requestSupafoneApi<LabsKnowledgeSyncResponse>(
3251
+ "DELETE",
3252
+ `/api/v1/agents/${encodeURIComponent(agentId)}/knowledge/website`,
3253
+ );
3254
+ }
3255
+
3256
+ /** Ask the managed server whether this agent is ready to activate. */
3257
+ readiness(agentKey: string, opts: GetLabsAgentOptions = {}): Promise<LabsAgentReadiness> {
3258
+ const q = new URLSearchParams();
3259
+ if (opts.agencyId) q.set("agency_id", opts.agencyId);
3260
+ const suffix = q.toString() ? `?${q}` : "";
3261
+ return this.sm.requestSupafoneApi<LabsAgentReadiness>(
3262
+ "GET",
3263
+ `/api/v1/labs/agents/${encodeURIComponent(agentKey)}/readiness${suffix}`,
3264
+ );
3265
+ }
3266
+
3267
+ /** Activate a ready hosted agent. */
3268
+ activate(
3269
+ agentKey: string,
3270
+ opts: GetLabsAgentOptions = {},
3271
+ ): Promise<LabsAgentLifecycleResponse> {
3272
+ const q = new URLSearchParams();
3273
+ if (opts.agencyId) q.set("agency_id", opts.agencyId);
3274
+ const suffix = q.toString() ? `?${q}` : "";
3275
+ return this.sm.requestSupafoneApi<LabsAgentLifecycleResponse>(
3276
+ "POST",
3277
+ `/api/v1/labs/agents/${encodeURIComponent(agentKey)}/activate${suffix}`,
3278
+ );
3279
+ }
3280
+
3281
+ /** Pause a hosted agent without deleting it. */
3282
+ pause(
3283
+ agentKey: string,
3284
+ opts: GetLabsAgentOptions = {},
3285
+ ): Promise<LabsAgentLifecycleResponse> {
3286
+ const q = new URLSearchParams();
3287
+ if (opts.agencyId) q.set("agency_id", opts.agencyId);
3288
+ const suffix = q.toString() ? `?${q}` : "";
3289
+ return this.sm.requestSupafoneApi<LabsAgentLifecycleResponse>(
3290
+ "POST",
3291
+ `/api/v1/labs/agents/${encodeURIComponent(agentKey)}/pause${suffix}`,
3292
+ );
3293
+ }
3294
+
3295
+ /** Start the native provider-neutral browser voice session for this agent. */
3296
+ startWebRtcCall(agentId: string): Promise<WebRtcCallResult> {
3297
+ return this.sm.startWebRtcCall({ agentId });
3298
+ }
3299
+
3300
+ /** Rebuild retrieval data from the agent's already saved knowledge sources. */
3301
+ reindexKnowledge(agentId: string): Promise<Record<string, unknown>> {
3302
+ return this.sm.requestSupafoneApi(
3303
+ "POST",
3304
+ `/api/v1/agents/${encodeURIComponent(agentId)}/knowledge/reindex`,
3305
+ );
3306
+ }
3307
+
3308
+ /** Upload and index a txt, md, csv, or PDF knowledge document. */
3309
+ uploadKnowledgeDocument(
3310
+ agentId: string,
3311
+ file: Uint8Array | ArrayBuffer | Blob,
3312
+ filename: string,
3313
+ ): Promise<{ agent: LabsAgentResponse }> {
3314
+ return this.sm.requestSupafoneUpload(
3315
+ `/api/v1/agents/${encodeURIComponent(agentId)}/knowledge/upload`,
3316
+ file,
3317
+ filename,
3318
+ );
3319
+ }
3320
+
3321
+ /** Delete one document and rebuild the agent's managed corpus. */
3322
+ deleteKnowledgeDocument(
3323
+ agentId: string,
3324
+ documentId: string,
3325
+ ): Promise<{ agent: LabsAgentResponse }> {
3326
+ return this.sm.requestSupafoneApi(
3327
+ "DELETE",
3328
+ `/api/v1/agents/${encodeURIComponent(agentId)}/knowledge/docs/${encodeURIComponent(documentId)}`,
3329
+ );
3330
+ }
3331
+
3332
+ /** Ask a grounded question against the same corpus used during calls. */
3333
+ chatKnowledge(
3334
+ agentId: string,
3335
+ message: string,
3336
+ history: LabsKnowledgeChatTurn[] = [],
3337
+ ): Promise<LabsKnowledgeChatResponse> {
3338
+ return this.sm.requestSupafoneApi(
3339
+ "POST",
3340
+ `/api/v1/agents/${encodeURIComponent(agentId)}/knowledge-chat`,
3341
+ { message, history },
3342
+ );
3343
+ }
3344
+
2906
3345
  /** Delete an agent. Optionally ask the backend to release assigned numbers. */
2907
3346
  delete(agentKey: string, opts: DeleteLabsAgentOptions = {}): Promise<DeleteLabsAgentResponse> {
2908
3347
  const q = new URLSearchParams();
@@ -3493,6 +3932,128 @@ class OptimizerNamespace {
3493
3932
  }
3494
3933
  }
3495
3934
 
3935
+ function outboundCallModePayload(config: OutboundCallModeConfig): Record<string, unknown> {
3936
+ const enabled = outboundModeBoolean(config.enabled, false, "enabled");
3937
+ if (!enabled) return { version: 1, enabled: false };
3938
+
3939
+ const observabilityInput = config.observability ?? {};
3940
+ if (typeof observabilityInput !== "object" || Array.isArray(observabilityInput)) {
3941
+ throw new SupafoneLabsError("outboundCallMode.observability must be an object");
3942
+ }
3943
+ if (
3944
+ observabilityInput.metadata !== undefined
3945
+ && (typeof observabilityInput.metadata !== "object"
3946
+ || observabilityInput.metadata === null
3947
+ || Array.isArray(observabilityInput.metadata))
3948
+ ) {
3949
+ throw new SupafoneLabsError("outboundCallMode.observability.metadata must be an object");
3950
+ }
3951
+ const observability = compact({
3952
+ enabled: outboundModeBoolean(observabilityInput.enabled, true, "observability.enabled"),
3953
+ include_trigger: outboundModeBoolean(
3954
+ observabilityInput.includeTrigger,
3955
+ true,
3956
+ "observability.includeTrigger",
3957
+ ),
3958
+ include_transitions: outboundModeBoolean(
3959
+ observabilityInput.includeTransitions,
3960
+ true,
3961
+ "observability.includeTransitions",
3962
+ ),
3963
+ include_termination_reason: outboundModeBoolean(
3964
+ observabilityInput.includeTerminationReason,
3965
+ true,
3966
+ "observability.includeTerminationReason",
3967
+ ),
3968
+ metadata: observabilityInput.metadata,
3969
+ });
3970
+ const payload: Record<string, unknown> = {
3971
+ version: 1,
3972
+ enabled: true,
3973
+ initial_mode: "mission",
3974
+ ivr_mode: "dynamic",
3975
+ transport_scope: "provider_agnostic",
3976
+ capability_policy: "fail_closed",
3977
+ auto_detect: outboundModeBoolean(config.autoDetect, true, "autoDetect"),
3978
+ dtmf_tool_enabled: outboundModeBoolean(config.dtmfToolEnabled, true, "dtmfToolEnabled"),
3979
+ max_duration_seconds: outboundModeInteger(
3980
+ config.maxDurationSeconds,
3981
+ "maxDurationSeconds",
3982
+ ),
3983
+ max_keypresses: outboundModeInteger(config.maxKeypresses, "maxKeypresses"),
3984
+ repeated_menu_limit: outboundModeInteger(config.repeatedMenuLimit, "repeatedMenuLimit"),
3985
+ no_progress_timeout_seconds: outboundModeInteger(
3986
+ config.noProgressTimeoutSeconds,
3987
+ "noProgressTimeoutSeconds",
3988
+ ),
3989
+ human_detection_enabled: outboundModeBoolean(
3990
+ config.humanDetectionEnabled,
3991
+ true,
3992
+ "humanDetectionEnabled",
3993
+ ),
3994
+ resume_on_human: outboundModeBoolean(config.resumeOnHuman, true, "resumeOnHuman"),
3995
+ observability,
3996
+ };
3997
+ payload.required_capabilities = outboundCallModeRequiredCapabilities(payload);
3998
+ return payload;
3999
+ }
4000
+
4001
+ function outboundModeBoolean(value: boolean | undefined, fallback: boolean, key: string): boolean {
4002
+ if (value === undefined) return fallback;
4003
+ if (typeof value !== "boolean") {
4004
+ throw new SupafoneLabsError(`outboundCallMode.${key} must be a boolean`);
4005
+ }
4006
+ return value;
4007
+ }
4008
+
4009
+ function outboundModeInteger(
4010
+ value: number | undefined,
4011
+ key: keyof typeof OUTBOUND_CALL_MODE_BOUNDS,
4012
+ ): number {
4013
+ const bounds = OUTBOUND_CALL_MODE_BOUNDS[key];
4014
+ const normalized = value ?? bounds.default;
4015
+ if (!Number.isInteger(normalized)) {
4016
+ throw new SupafoneLabsError(`outboundCallMode.${key} must be an integer`);
4017
+ }
4018
+ if (normalized < bounds.min || normalized > bounds.max) {
4019
+ throw new SupafoneLabsError(
4020
+ `outboundCallMode.${key} must be between ${bounds.min} and ${bounds.max}`,
4021
+ );
4022
+ }
4023
+ return normalized;
4024
+ }
4025
+
4026
+ function outboundCallModeRequiredCapabilities(payload: Record<string, unknown>): string[] {
4027
+ const required = ["state_persistence"];
4028
+ if (payload.dtmf_tool_enabled !== false) required.unshift("dtmf");
4029
+ if (payload.human_detection_enabled !== false || payload.resume_on_human !== false) {
4030
+ required.push("human_detection");
4031
+ }
4032
+ const observability = payload.observability as Record<string, unknown> | undefined;
4033
+ if (observability?.enabled === true) required.push("observability");
4034
+ return required;
4035
+ }
4036
+
4037
+ function labsAgentMetadataPayload(input: CreateLabsAgentRequest): Record<string, unknown> | undefined {
4038
+ const metadata = { ...(input.metadata ?? {}) };
4039
+ const mode = input.outboundCallMode ?? input.outbound_call_mode;
4040
+ if (mode !== undefined) metadata.outbound_call_mode = outboundCallModePayload(mode);
4041
+ return Object.keys(metadata).length ? metadata : undefined;
4042
+ }
4043
+
4044
+ function labsAgentUpdatePayload(input: UpdateLabsAgentRequest): Record<string, unknown> {
4045
+ return labsAgentPayload(input as CreateLabsAgentRequest);
4046
+ }
4047
+
4048
+ function campaignSettingsPayload(
4049
+ settings?: Record<string, unknown>,
4050
+ mode?: OutboundCallModeConfig,
4051
+ ): Record<string, unknown> {
4052
+ const payload = { ...(settings ?? {}) };
4053
+ if (mode !== undefined) payload.outbound_call_mode = outboundCallModePayload(mode);
4054
+ return payload;
4055
+ }
4056
+
3496
4057
  function labsAgentPayload(input: CreateLabsAgentRequest): Record<string, unknown> {
3497
4058
  const fixedLanguage = input.preferred_language ?? input.preferredLanguage ?? input.language;
3498
4059
  return compact({
@@ -3542,7 +4103,7 @@ function labsAgentPayload(input: CreateLabsAgentRequest): Record<string, unknown
3542
4103
  ultravox: input.ultravox ? ultravoxPayload(input.ultravox) : undefined,
3543
4104
  voice_watcher: input.voice_watcher ?? input.voiceWatcher,
3544
4105
  voice_watcher_model: input.voice_watcher_model ?? input.voiceWatcherModel,
3545
- metadata: input.metadata,
4106
+ metadata: labsAgentMetadataPayload(input),
3546
4107
  });
3547
4108
  }
3548
4109
 
@@ -3795,6 +4356,9 @@ function languageProfilesPayload(
3795
4356
  input?: LabsLanguageVoiceProfile[],
3796
4357
  ): Record<string, unknown>[] | undefined {
3797
4358
  if (!Array.isArray(input)) return undefined;
4359
+ if (input.length > 4) {
4360
+ throw new SupafoneLabsError("languageProfiles supports at most four profiles");
4361
+ }
3798
4362
  const profiles = input.map((profile) => compact({
3799
4363
  language: profile.language,
3800
4364
  language_hint: profile.language_hint ?? profile.languageHint,