supafone-labs 0.4.14 → 0.5.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 CHANGED
@@ -132,6 +132,26 @@ The first configured language owns the opening, and a non-English primary
132
132
  greeting is translated during provisioning. See
133
133
  [Live Language and Voice Routing](../gitbook/live-language-voice-routing.md).
134
134
 
135
+ Outbound agents can opt into temporary, bounded phone-tree navigation without
136
+ changing their normal campaign objective:
137
+
138
+ ```ts
139
+ await supafone.labs.agents.createOutbound({
140
+ name: "Benefits verification",
141
+ outboundCallMode: {
142
+ enabled: true,
143
+ maxDurationSeconds: 180,
144
+ maxKeypresses: 12,
145
+ },
146
+ });
147
+ ```
148
+
149
+ The SDK stores this provider-neutral contract and exposes fail-closed adapter
150
+ readiness checks. Supafone-managed, Twilio, Telnyx, Plivo, SignalWire, and
151
+ SIP/BYOC use the same public shape; private detection and DTMF execution stay in
152
+ the managed runtime. See
153
+ [Outbound IVR Call Mode](../docs/outbound-ivr-call-mode.md).
154
+
135
155
  BYOK is advanced and split into three independent lanes:
136
156
 
137
157
  | Lane | Examples |
@@ -622,6 +622,95 @@ export interface LabsUltravoxRuntime {
622
622
  sip?: LabsCustomSipConfig;
623
623
  [extra: string]: unknown;
624
624
  }
625
+ export interface OutboundCallModeObservability {
626
+ enabled?: boolean;
627
+ includeTrigger?: boolean;
628
+ includeTransitions?: boolean;
629
+ includeTerminationReason?: boolean;
630
+ metadata?: Record<string, unknown>;
631
+ }
632
+ /** Public, provider-neutral controls for temporary outbound IVR navigation. */
633
+ export interface OutboundCallModeConfig {
634
+ enabled?: boolean;
635
+ autoDetect?: boolean;
636
+ dtmfToolEnabled?: boolean;
637
+ maxDurationSeconds?: number;
638
+ maxKeypresses?: number;
639
+ repeatedMenuLimit?: number;
640
+ noProgressTimeoutSeconds?: number;
641
+ humanDetectionEnabled?: boolean;
642
+ resumeOnHuman?: boolean;
643
+ observability?: OutboundCallModeObservability;
644
+ }
645
+ /** Capabilities positively reported by the active telephony/runtime adapter. */
646
+ export interface OutboundCallModeCapabilities {
647
+ provider?: string;
648
+ dtmf?: boolean;
649
+ statePersistence?: boolean;
650
+ humanDetection?: boolean;
651
+ observability?: boolean;
652
+ metadata?: Record<string, unknown>;
653
+ }
654
+ export interface OutboundCallModeReadiness {
655
+ ready: boolean;
656
+ status: "disabled" | "ready" | "unknown" | "unsupported";
657
+ provider: string;
658
+ transportFamily: string;
659
+ requiredCapabilities: string[];
660
+ missingCapabilities: string[];
661
+ unsupportedCapabilities: string[];
662
+ reasons: string[];
663
+ }
664
+ export declare const OUTBOUND_CALL_MODE_BOUNDS: {
665
+ readonly maxDurationSeconds: {
666
+ readonly default: 180;
667
+ readonly min: 30;
668
+ readonly max: 900;
669
+ };
670
+ readonly maxKeypresses: {
671
+ readonly default: 12;
672
+ readonly min: 1;
673
+ readonly max: 64;
674
+ };
675
+ readonly repeatedMenuLimit: {
676
+ readonly default: 3;
677
+ readonly min: 1;
678
+ readonly max: 10;
679
+ };
680
+ readonly noProgressTimeoutSeconds: {
681
+ readonly default: 30;
682
+ readonly min: 5;
683
+ readonly max: 120;
684
+ };
685
+ };
686
+ export declare const OUTBOUND_CALL_MODE_DEFAULTS: {
687
+ readonly enabled: false;
688
+ readonly autoDetect: true;
689
+ readonly dtmfToolEnabled: true;
690
+ readonly maxDurationSeconds: 180;
691
+ readonly maxKeypresses: 12;
692
+ readonly repeatedMenuLimit: 3;
693
+ readonly noProgressTimeoutSeconds: 30;
694
+ readonly humanDetectionEnabled: true;
695
+ readonly resumeOnHuman: true;
696
+ readonly observability: {
697
+ readonly enabled: true;
698
+ readonly includeTrigger: true;
699
+ readonly includeTransitions: true;
700
+ readonly includeTerminationReason: true;
701
+ };
702
+ };
703
+ export interface OutboundCallModeProviderProfile {
704
+ contractSupported: boolean;
705
+ execution: "adapter_runtime_dependent";
706
+ capabilityPolicy: "fail_closed";
707
+ transportFamily: string;
708
+ providers: readonly string[];
709
+ requiredCapabilities: readonly string[];
710
+ }
711
+ export declare const OUTBOUND_CALL_MODE_PROVIDER_MATRIX: Record<string, OutboundCallModeProviderProfile>;
712
+ export declare function outboundCallModeProviderProfile(provider: string): OutboundCallModeProviderProfile;
713
+ export declare function outboundCallModeReadiness(config?: OutboundCallModeConfig, capabilities?: OutboundCallModeCapabilities): OutboundCallModeReadiness;
625
714
  export interface CreateLabsAgentRequest {
626
715
  agencyId?: string;
627
716
  agency_id?: string;
@@ -706,6 +795,9 @@ export interface CreateLabsAgentRequest {
706
795
  voice_watcher?: boolean;
707
796
  voiceWatcherModel?: string;
708
797
  voice_watcher_model?: string;
798
+ /** Opt-in outbound IVR mode. Execution depends on adapter-reported readiness. */
799
+ outboundCallMode?: OutboundCallModeConfig;
800
+ outbound_call_mode?: OutboundCallModeConfig;
709
801
  metadata?: Record<string, unknown>;
710
802
  }
711
803
  export interface ListLabsAgentsOptions {
@@ -728,6 +820,21 @@ export interface DeleteLabsAgentResponse {
728
820
  released_numbers?: unknown[];
729
821
  [extra: string]: unknown;
730
822
  }
823
+ export type LabsAgentLifecycle = "draft" | "active" | "paused" | "archived" | string;
824
+ export interface LabsAgentReadiness {
825
+ ready: boolean;
826
+ status?: string;
827
+ checks?: Array<Record<string, unknown>>;
828
+ [extra: string]: unknown;
829
+ }
830
+ export type UpdateLabsAgentRequest = Partial<CreateLabsAgentRequest>;
831
+ export interface LabsAgentLifecycleResponse {
832
+ success?: boolean;
833
+ agent?: LabsAgentResponse;
834
+ lifecycle?: LabsAgentLifecycle;
835
+ readiness?: LabsAgentReadiness;
836
+ [extra: string]: unknown;
837
+ }
731
838
  export interface LabsCapabilitiesResponse {
732
839
  product: string;
733
840
  api_namespace: string;
@@ -1068,6 +1175,8 @@ export interface LabsAgentResponse {
1068
1175
  display_name?: string;
1069
1176
  runtime_mode?: string;
1070
1177
  preset_key?: string;
1178
+ status?: string;
1179
+ lifecycle?: LabsAgentLifecycle;
1071
1180
  profile?: Record<string, unknown>;
1072
1181
  runtime?: Record<string, unknown>;
1073
1182
  [extra: string]: unknown;
@@ -1100,6 +1209,9 @@ export interface ListLabsAgentsResponse {
1100
1209
  }
1101
1210
  export interface GetLabsAgentResponse {
1102
1211
  agent: LabsAgentResponse;
1212
+ runtime?: Record<string, unknown>;
1213
+ widget?: Record<string, unknown>;
1214
+ readiness?: LabsAgentReadiness;
1103
1215
  }
1104
1216
  export interface STTResult {
1105
1217
  transcript: string;
@@ -1625,6 +1737,14 @@ export interface CampaignLiveView {
1625
1737
  portal_url: string;
1626
1738
  stats?: Record<string, unknown> | null;
1627
1739
  }
1740
+ export interface CampaignCreateInput {
1741
+ name?: string;
1742
+ goal?: string;
1743
+ agentId?: string;
1744
+ accountId?: string;
1745
+ settings?: Record<string, unknown>;
1746
+ outboundCallMode?: OutboundCallModeConfig;
1747
+ }
1628
1748
  export interface CampaignUpdateInput {
1629
1749
  name?: string;
1630
1750
  goal?: string;
@@ -1636,6 +1756,7 @@ export interface CampaignUpdateInput {
1636
1756
  delay_hours: number;
1637
1757
  }[];
1638
1758
  settings?: Record<string, unknown>;
1759
+ outboundCallMode?: OutboundCallModeConfig;
1639
1760
  }
1640
1761
  export interface BrandScanResult {
1641
1762
  url: string;
@@ -1699,12 +1820,7 @@ declare class CampaignsNamespace {
1699
1820
  }): Promise<{
1700
1821
  campaigns: CampaignSummary[];
1701
1822
  }>;
1702
- create(opts?: {
1703
- name?: string;
1704
- goal?: string;
1705
- agentId?: string;
1706
- accountId?: string;
1707
- }): Promise<{
1823
+ create(opts?: CampaignCreateInput): Promise<{
1708
1824
  campaign: CampaignSummary;
1709
1825
  }>;
1710
1826
  get(campaignId: string): Promise<{
@@ -1912,6 +2028,14 @@ declare class LabsAgentsNamespace {
1912
2028
  list(opts?: ListLabsAgentsOptions): Promise<ListLabsAgentsResponse>;
1913
2029
  /** Fetch one durable agent by key. */
1914
2030
  get(agentKey: string, opts?: GetLabsAgentOptions): Promise<GetLabsAgentResponse>;
2031
+ /** Update an existing durable agent without changing omitted fields. */
2032
+ update(agentKey: string, input: UpdateLabsAgentRequest, opts?: GetLabsAgentOptions): Promise<LabsAgentLifecycleResponse>;
2033
+ /** Ask the managed server whether this agent is ready to activate. */
2034
+ readiness(agentKey: string, opts?: GetLabsAgentOptions): Promise<LabsAgentReadiness>;
2035
+ /** Activate a ready hosted agent. */
2036
+ activate(agentKey: string, opts?: GetLabsAgentOptions): Promise<LabsAgentLifecycleResponse>;
2037
+ /** Pause a hosted agent without deleting it. */
2038
+ pause(agentKey: string, opts?: GetLabsAgentOptions): Promise<LabsAgentLifecycleResponse>;
1915
2039
  /** Delete an agent. Optionally ask the backend to release assigned numbers. */
1916
2040
  delete(agentKey: string, opts?: DeleteLabsAgentOptions): Promise<DeleteLabsAgentResponse>;
1917
2041
  }
package/dist/cjs/index.js CHANGED
@@ -21,8 +21,131 @@
21
21
  * });
22
22
  */
23
23
  Object.defineProperty(exports, "__esModule", { value: true });
24
- exports.Supafone = exports.SupafoneLabs = exports.SupafoneLabsError = void 0;
24
+ exports.Supafone = exports.SupafoneLabs = exports.SupafoneLabsError = exports.OUTBOUND_CALL_MODE_PROVIDER_MATRIX = exports.OUTBOUND_CALL_MODE_DEFAULTS = exports.OUTBOUND_CALL_MODE_BOUNDS = void 0;
25
+ exports.outboundCallModeProviderProfile = outboundCallModeProviderProfile;
26
+ exports.outboundCallModeReadiness = outboundCallModeReadiness;
25
27
  exports.generateCallStages = generateCallStages;
28
+ exports.OUTBOUND_CALL_MODE_BOUNDS = {
29
+ maxDurationSeconds: { default: 180, min: 30, max: 900 },
30
+ maxKeypresses: { default: 12, min: 1, max: 64 },
31
+ repeatedMenuLimit: { default: 3, min: 1, max: 10 },
32
+ noProgressTimeoutSeconds: { default: 30, min: 5, max: 120 },
33
+ };
34
+ exports.OUTBOUND_CALL_MODE_DEFAULTS = {
35
+ enabled: false,
36
+ autoDetect: true,
37
+ dtmfToolEnabled: true,
38
+ maxDurationSeconds: 180,
39
+ maxKeypresses: 12,
40
+ repeatedMenuLimit: 3,
41
+ noProgressTimeoutSeconds: 30,
42
+ humanDetectionEnabled: true,
43
+ resumeOnHuman: true,
44
+ observability: {
45
+ enabled: true,
46
+ includeTrigger: true,
47
+ includeTransitions: true,
48
+ includeTerminationReason: true,
49
+ },
50
+ };
51
+ const OUTBOUND_PROVIDER_PROFILE_BASE = {
52
+ contractSupported: true,
53
+ execution: "adapter_runtime_dependent",
54
+ capabilityPolicy: "fail_closed",
55
+ requiredCapabilities: ["dtmf", "state_persistence", "human_detection"],
56
+ };
57
+ exports.OUTBOUND_CALL_MODE_PROVIDER_MATRIX = {
58
+ supafoneManaged: {
59
+ ...OUTBOUND_PROVIDER_PROFILE_BASE,
60
+ transportFamily: "supafone_managed",
61
+ providers: ["supafone", "supafone_managed", "managed"],
62
+ },
63
+ twilio: {
64
+ ...OUTBOUND_PROVIDER_PROFILE_BASE,
65
+ transportFamily: "twilio",
66
+ providers: ["twilio", "byo_twilio"],
67
+ },
68
+ telnyx: {
69
+ ...OUTBOUND_PROVIDER_PROFILE_BASE,
70
+ transportFamily: "telnyx",
71
+ providers: ["telnyx", "byo_telnyx"],
72
+ },
73
+ plivo: {
74
+ ...OUTBOUND_PROVIDER_PROFILE_BASE,
75
+ transportFamily: "plivo",
76
+ providers: ["plivo", "byo_plivo"],
77
+ },
78
+ signalWire: {
79
+ ...OUTBOUND_PROVIDER_PROFILE_BASE,
80
+ transportFamily: "signalwire",
81
+ providers: ["signalwire", "signal_wire", "byo_signalwire"],
82
+ },
83
+ sipByoc: {
84
+ ...OUTBOUND_PROVIDER_PROFILE_BASE,
85
+ transportFamily: "sip_byoc",
86
+ providers: ["sip", "custom_sip", "byo_sip", "byoc", "sip_byoc"],
87
+ },
88
+ };
89
+ function outboundCallModeProviderProfile(provider) {
90
+ const normalized = String(provider ?? "").trim().toLowerCase();
91
+ for (const profile of Object.values(exports.OUTBOUND_CALL_MODE_PROVIDER_MATRIX)) {
92
+ if (profile.providers.includes(normalized)) {
93
+ return { ...profile, providers: [...profile.providers] };
94
+ }
95
+ }
96
+ return {
97
+ contractSupported: false,
98
+ execution: "adapter_runtime_dependent",
99
+ capabilityPolicy: "fail_closed",
100
+ transportFamily: "unknown",
101
+ providers: normalized ? [normalized] : [],
102
+ requiredCapabilities: ["dtmf", "state_persistence", "human_detection"],
103
+ };
104
+ }
105
+ function outboundCallModeReadiness(config, capabilities) {
106
+ const provider = String(capabilities?.provider ?? "");
107
+ const profile = outboundCallModeProviderProfile(provider);
108
+ const normalized = outboundCallModePayload(config ?? {});
109
+ if (normalized.enabled !== true) {
110
+ return {
111
+ ready: true,
112
+ status: "disabled",
113
+ provider,
114
+ transportFamily: profile.transportFamily,
115
+ requiredCapabilities: [],
116
+ missingCapabilities: [],
117
+ unsupportedCapabilities: [],
118
+ reasons: [],
119
+ };
120
+ }
121
+ const requiredCapabilities = [...normalized.required_capabilities];
122
+ const reported = {
123
+ dtmf: capabilities?.dtmf,
124
+ state_persistence: capabilities?.statePersistence,
125
+ human_detection: capabilities?.humanDetection,
126
+ observability: capabilities?.observability,
127
+ };
128
+ const missingCapabilities = requiredCapabilities.filter((key) => reported[key] === undefined);
129
+ const unsupportedCapabilities = requiredCapabilities.filter((key) => reported[key] === false);
130
+ const status = unsupportedCapabilities.length
131
+ ? "unsupported"
132
+ : missingCapabilities.length
133
+ ? "unknown"
134
+ : "ready";
135
+ const reasons = unsupportedCapabilities.length
136
+ ? unsupportedCapabilities.map((key) => `Adapter reports ${key} is unsupported.`)
137
+ : missingCapabilities.map((key) => `Adapter did not report ${key} support.`);
138
+ return {
139
+ ready: status === "ready",
140
+ status,
141
+ provider,
142
+ transportFamily: profile.transportFamily,
143
+ requiredCapabilities,
144
+ missingCapabilities,
145
+ unsupportedCapabilities,
146
+ reasons,
147
+ };
148
+ }
26
149
  class SupafoneLabsError extends Error {
27
150
  status;
28
151
  body;
@@ -770,18 +893,26 @@ class CampaignsNamespace {
770
893
  const query = opts.accountId ? `?${new URLSearchParams({ account_id: opts.accountId })}` : "";
771
894
  return this.sm.requestAccountApi("GET", `/api/v1/campaigns${query}`);
772
895
  }
773
- create(opts = {}) {
774
- return this.sm.requestAccountApi("POST", "/api/v1/campaigns", compact({
896
+ async create(opts = {}) {
897
+ const created = await this.sm.requestAccountApi("POST", "/api/v1/campaigns", compact({
775
898
  name: opts.name ?? "New campaign",
776
899
  goal: opts.goal ?? "book",
777
900
  agent_id: opts.agentId,
778
901
  account_id: opts.accountId,
779
902
  }));
903
+ const settings = campaignSettingsPayload(opts.settings, opts.outboundCallMode);
904
+ if (!Object.keys(settings).length)
905
+ return created;
906
+ if (!created.campaign?.id) {
907
+ throw new SupafoneLabsError("Campaign was created but no campaign id was returned; outbound call mode was not saved.");
908
+ }
909
+ return this.update(created.campaign.id, { settings });
780
910
  }
781
911
  get(campaignId) {
782
912
  return this.sm.requestAccountApi("GET", `/api/v1/campaigns/${encodeURIComponent(campaignId)}`);
783
913
  }
784
914
  update(campaignId, input) {
915
+ const settings = campaignSettingsPayload(input.settings, input.outboundCallMode);
785
916
  const payload = compact({
786
917
  name: input.name,
787
918
  goal: input.goal,
@@ -789,10 +920,10 @@ class CampaignsNamespace {
789
920
  email_subject: input.emailSubject,
790
921
  email_body: input.emailBody,
791
922
  cadence: input.cadence,
792
- settings: input.settings,
923
+ settings: Object.keys(settings).length ? settings : undefined,
793
924
  });
794
925
  if (!Object.keys(payload).length) {
795
- throw new SupafoneLabsError("Nothing to update — pass name, goal, agentId, emailSubject, emailBody, cadence, or settings");
926
+ throw new SupafoneLabsError("Nothing to update — pass name, goal, agentId, emailSubject, emailBody, cadence, settings, or outboundCallMode");
796
927
  }
797
928
  return this.sm.requestAccountApi("PUT", `/api/v1/campaigns/${encodeURIComponent(campaignId)}`, payload);
798
929
  }
@@ -1119,6 +1250,38 @@ class LabsAgentsNamespace {
1119
1250
  const suffix = q.toString() ? `?${q}` : "";
1120
1251
  return this.sm.requestSupafoneApi("GET", `/api/v1/labs/agents/${encodeURIComponent(agentKey)}${suffix}`);
1121
1252
  }
1253
+ /** Update an existing durable agent without changing omitted fields. */
1254
+ update(agentKey, input, opts = {}) {
1255
+ const q = new URLSearchParams();
1256
+ if (opts.agencyId)
1257
+ q.set("agency_id", opts.agencyId);
1258
+ const suffix = q.toString() ? `?${q}` : "";
1259
+ return this.sm.requestSupafoneApi("PATCH", `/api/v1/labs/agents/${encodeURIComponent(agentKey)}${suffix}`, labsAgentUpdatePayload(input));
1260
+ }
1261
+ /** Ask the managed server whether this agent is ready to activate. */
1262
+ readiness(agentKey, opts = {}) {
1263
+ const q = new URLSearchParams();
1264
+ if (opts.agencyId)
1265
+ q.set("agency_id", opts.agencyId);
1266
+ const suffix = q.toString() ? `?${q}` : "";
1267
+ return this.sm.requestSupafoneApi("GET", `/api/v1/labs/agents/${encodeURIComponent(agentKey)}/readiness${suffix}`);
1268
+ }
1269
+ /** Activate a ready hosted agent. */
1270
+ activate(agentKey, opts = {}) {
1271
+ const q = new URLSearchParams();
1272
+ if (opts.agencyId)
1273
+ q.set("agency_id", opts.agencyId);
1274
+ const suffix = q.toString() ? `?${q}` : "";
1275
+ return this.sm.requestSupafoneApi("POST", `/api/v1/labs/agents/${encodeURIComponent(agentKey)}/activate${suffix}`);
1276
+ }
1277
+ /** Pause a hosted agent without deleting it. */
1278
+ pause(agentKey, opts = {}) {
1279
+ const q = new URLSearchParams();
1280
+ if (opts.agencyId)
1281
+ q.set("agency_id", opts.agencyId);
1282
+ const suffix = q.toString() ? `?${q}` : "";
1283
+ return this.sm.requestSupafoneApi("POST", `/api/v1/labs/agents/${encodeURIComponent(agentKey)}/pause${suffix}`);
1284
+ }
1122
1285
  /** Delete an agent. Optionally ask the backend to release assigned numbers. */
1123
1286
  delete(agentKey, opts = {}) {
1124
1287
  const q = new URLSearchParams();
@@ -1664,6 +1827,94 @@ class OptimizerNamespace {
1664
1827
  return this.sm.request("GET", `/v1/optimizer/reports?agent=${encodeURIComponent(agent)}&limit=${limit}`);
1665
1828
  }
1666
1829
  }
1830
+ function outboundCallModePayload(config) {
1831
+ const enabled = outboundModeBoolean(config.enabled, false, "enabled");
1832
+ if (!enabled)
1833
+ return { version: 1, enabled: false };
1834
+ const observabilityInput = config.observability ?? {};
1835
+ if (typeof observabilityInput !== "object" || Array.isArray(observabilityInput)) {
1836
+ throw new SupafoneLabsError("outboundCallMode.observability must be an object");
1837
+ }
1838
+ if (observabilityInput.metadata !== undefined
1839
+ && (typeof observabilityInput.metadata !== "object"
1840
+ || observabilityInput.metadata === null
1841
+ || Array.isArray(observabilityInput.metadata))) {
1842
+ throw new SupafoneLabsError("outboundCallMode.observability.metadata must be an object");
1843
+ }
1844
+ const observability = compact({
1845
+ enabled: outboundModeBoolean(observabilityInput.enabled, true, "observability.enabled"),
1846
+ include_trigger: outboundModeBoolean(observabilityInput.includeTrigger, true, "observability.includeTrigger"),
1847
+ include_transitions: outboundModeBoolean(observabilityInput.includeTransitions, true, "observability.includeTransitions"),
1848
+ include_termination_reason: outboundModeBoolean(observabilityInput.includeTerminationReason, true, "observability.includeTerminationReason"),
1849
+ metadata: observabilityInput.metadata,
1850
+ });
1851
+ const payload = {
1852
+ version: 1,
1853
+ enabled: true,
1854
+ initial_mode: "mission",
1855
+ ivr_mode: "dynamic",
1856
+ transport_scope: "provider_agnostic",
1857
+ capability_policy: "fail_closed",
1858
+ auto_detect: outboundModeBoolean(config.autoDetect, true, "autoDetect"),
1859
+ dtmf_tool_enabled: outboundModeBoolean(config.dtmfToolEnabled, true, "dtmfToolEnabled"),
1860
+ max_duration_seconds: outboundModeInteger(config.maxDurationSeconds, "maxDurationSeconds"),
1861
+ max_keypresses: outboundModeInteger(config.maxKeypresses, "maxKeypresses"),
1862
+ repeated_menu_limit: outboundModeInteger(config.repeatedMenuLimit, "repeatedMenuLimit"),
1863
+ no_progress_timeout_seconds: outboundModeInteger(config.noProgressTimeoutSeconds, "noProgressTimeoutSeconds"),
1864
+ human_detection_enabled: outboundModeBoolean(config.humanDetectionEnabled, true, "humanDetectionEnabled"),
1865
+ resume_on_human: outboundModeBoolean(config.resumeOnHuman, true, "resumeOnHuman"),
1866
+ observability,
1867
+ };
1868
+ payload.required_capabilities = outboundCallModeRequiredCapabilities(payload);
1869
+ return payload;
1870
+ }
1871
+ function outboundModeBoolean(value, fallback, key) {
1872
+ if (value === undefined)
1873
+ return fallback;
1874
+ if (typeof value !== "boolean") {
1875
+ throw new SupafoneLabsError(`outboundCallMode.${key} must be a boolean`);
1876
+ }
1877
+ return value;
1878
+ }
1879
+ function outboundModeInteger(value, key) {
1880
+ const bounds = exports.OUTBOUND_CALL_MODE_BOUNDS[key];
1881
+ const normalized = value ?? bounds.default;
1882
+ if (!Number.isInteger(normalized)) {
1883
+ throw new SupafoneLabsError(`outboundCallMode.${key} must be an integer`);
1884
+ }
1885
+ if (normalized < bounds.min || normalized > bounds.max) {
1886
+ throw new SupafoneLabsError(`outboundCallMode.${key} must be between ${bounds.min} and ${bounds.max}`);
1887
+ }
1888
+ return normalized;
1889
+ }
1890
+ function outboundCallModeRequiredCapabilities(payload) {
1891
+ const required = ["state_persistence"];
1892
+ if (payload.dtmf_tool_enabled !== false)
1893
+ required.unshift("dtmf");
1894
+ if (payload.human_detection_enabled !== false || payload.resume_on_human !== false) {
1895
+ required.push("human_detection");
1896
+ }
1897
+ const observability = payload.observability;
1898
+ if (observability?.enabled === true)
1899
+ required.push("observability");
1900
+ return required;
1901
+ }
1902
+ function labsAgentMetadataPayload(input) {
1903
+ const metadata = { ...(input.metadata ?? {}) };
1904
+ const mode = input.outboundCallMode ?? input.outbound_call_mode;
1905
+ if (mode !== undefined)
1906
+ metadata.outbound_call_mode = outboundCallModePayload(mode);
1907
+ return Object.keys(metadata).length ? metadata : undefined;
1908
+ }
1909
+ function labsAgentUpdatePayload(input) {
1910
+ return labsAgentPayload(input);
1911
+ }
1912
+ function campaignSettingsPayload(settings, mode) {
1913
+ const payload = { ...(settings ?? {}) };
1914
+ if (mode !== undefined)
1915
+ payload.outbound_call_mode = outboundCallModePayload(mode);
1916
+ return payload;
1917
+ }
1667
1918
  function labsAgentPayload(input) {
1668
1919
  const fixedLanguage = input.preferred_language ?? input.preferredLanguage ?? input.language;
1669
1920
  return compact({
@@ -1710,7 +1961,7 @@ function labsAgentPayload(input) {
1710
1961
  ultravox: input.ultravox ? ultravoxPayload(input.ultravox) : undefined,
1711
1962
  voice_watcher: input.voice_watcher ?? input.voiceWatcher,
1712
1963
  voice_watcher_model: input.voice_watcher_model ?? input.voiceWatcherModel,
1713
- metadata: input.metadata,
1964
+ metadata: labsAgentMetadataPayload(input),
1714
1965
  });
1715
1966
  }
1716
1967
  function hostedListQuery(opts) {